From 46cbe571eb49486ad93ede60b1b8af607f3e68c7 Mon Sep 17 00:00:00 2001 From: pohui Date: Wed, 30 Oct 2024 09:54:24 +0100 Subject: [PATCH] init --- .gitattributes | 4 + .gitignore | 33 + .gitmodules | 6 + .gitpod.yml | 2 + .semgrepignore | 2 + CONTRIBUTING.md | 47 + Dockerfile | 54 + LICENSE | 675 + SECURITY.md | 12 + arch/esp32/esp32.ini | 67 + arch/esp32/esp32c3.ini | 6 + arch/esp32/esp32c6.ini | 40 + arch/esp32/esp32s2.ini | 19 + arch/esp32/esp32s3.ini | 6 + arch/nrf52/cpp_overrides/lfs_util.h | 208 + arch/nrf52/nrf52.ini | 27 + arch/nrf52/nrf52832.ini | 7 + arch/nrf52/nrf52840.ini | 78 + arch/portduino/portduino.ini | 37 + arch/rp2xx0/rp2040.ini | 25 + arch/rp2xx0/rp2350.ini | 24 + arch/stm32/stm32.ini | 37 + ...stic_nRF52_factory_erase_v3_S140_6.1.0.uf2 | Bin 0 -> 126976 bytes ...stic_nRF52_factory_erase_v3_S140_7.3.0.uf2 | Bin 0 -> 122368 bytes bin/build-esp32.sh | 40 + bin/build-native.sh | 31 + bin/build-nrf52.sh | 53 + bin/build-rpi2040.sh | 33 + bin/build-stm32.sh | 29 + bin/build-userprefs-json.py | 48 + bin/buildinfo.py | 8 + bin/bump_version.py | 16 + bin/check-all.sh | 26 + bin/check-dependencies.sh | 23 + bin/config-dist.yaml | 178 + bin/config.d/display-waveshare-2.8.yaml | 18 + bin/config.d/lora-waveshare-sxxx.yaml | 8 + bin/device-install.bat | 55 + bin/device-install.sh | 71 + bin/device-update.bat | 40 + bin/device-update.sh | 54 + bin/dump-ram-users.sh | 5 + bin/exception_decoder.py | 390 + bin/gen-images.sh | 19 + bin/generate_ci_matrix.py | 46 + ...stic_6.1.0_bootloader-0.9.2_s140_6.1.1.hex | 11744 +++++++++++++++ ...stic_6.1.0_bootloader-0.9.2_s140_6.1.1.zip | Bin 0 -> 190874 bytes ...stic_7.3.0_bootloader-0.9.2_s140_7.3.0.hex | 11851 ++++++++++++++++ ...stic_7.3.0_bootloader-0.9.2_s140_7.3.0.zip | Bin 0 -> 192586 bytes ...Meshtastic_6.1.0_bootloader-0.9.2_nosd.uf2 | Bin 0 -> 74752 bytes ...Meshtastic_7.3.0_bootloader-0.9.2_nosd.uf2 | Bin 0 -> 74752 bytes bin/genpartitions.py | 41 + bin/lilygo_techo_bootloader-0.6.1.zip | Bin 0 -> 190873 bytes bin/mergehex | Bin 0 -> 2102544 bytes bin/meshtasticd.service | 16 + bin/native-gdbserver.sh | 5 + bin/native-install.sh | 10 + bin/native-run.sh | 5 + bin/platformio-custom.py | 99 + bin/promote-release.sh | 14 + bin/read-system-info.sh | 3 + bin/readprops.py | 42 + bin/regen-protos.bat | 1 + bin/regen-protos.sh | 11 + bin/s140_nrf52_7.3.0_softdevice.hex | 9726 +++++++++++++ bin/setup-python-for-esp-debug.sh | 12 + bin/test-simulator.sh | 11 + bin/uf2-convert.bat | 2 + bin/uf2conv.py | 381 + ...ate-lilygo_techo_bootloader-0.6.1_nosd.uf2 | Bin 0 -> 75264 bytes bin/view-map.sh | 4 + bin/wio_tracker_bootloader_update.bin | 30 + boards/CDEBYTE_EoRa-S3.json | 38 + boards/ESP32-S3-WROOM-1-N4.json | 39 + boards/bpi_picow_esp32_s3.json | 38 + boards/canaryone.json | 52 + boards/eink0.1.json | 47 + boards/esp32-s3-pico.json | 40 + boards/heltec_mesh_node_t114.json | 53 + boards/heltec_vision_master_e213.json | 42 + boards/heltec_vision_master_e290.json | 42 + boards/heltec_vision_master_t190.json | 42 + boards/heltec_wireless_tracker.json | 38 + boards/icarus.json | 41 + boards/me25ls01-4y10td.json | 58 + boards/ms24sf1.json | 58 + boards/my-esp32s3-diy-oled.json | 41 + boards/my_esp32s3_diy_eink.json | 41 + boards/nano-g2-ultra.json | 51 + boards/nordic_pca10059.json | 52 + boards/nrf52840_dk.json | 47 + boards/promicro-nrf52840.json | 52 + boards/seeed-sensecap-indicator.json | 42 + boards/seeed-xiao-s3.json | 41 + boards/station-g2.json | 41 + boards/t-deck.json | 41 + boards/t-echo.json | 53 + boards/t-watch-s3.json | 43 + boards/tbeam-s3-core.json | 37 + boards/tlora-t3s3-v1.json | 38 + boards/tracker-t1000-e.json | 58 + boards/unphone.json | 46 + boards/wio-sdk-wm1110.json | 51 + boards/wio-t1000-s.json | 58 + boards/wio-tracker-wm1110.json | 58 + boards/wiphone.json | 34 + boards/wiscore_rak11200.json | 27 + boards/wiscore_rak11300.json | 40 + boards/wiscore_rak3172.json | 30 + boards/wiscore_rak4600.json | 50 + boards/wiscore_rak4631.json | 52 + boards/xiao_ble_sense.json | 58 + data/static/.gitkeep | 0 docker-compose.yml | 13 + extra_scripts/README.md | 3 + extra_scripts/disable_adafruit_usb.py | 28 + images/compass.png | Bin 0 -> 594 bytes images/face-24px.svg | 1 + images/face.png | Bin 0 -> 225 bytes images/location_searching-24px.svg | 1 + images/pin.png | Bin 0 -> 203 bytes images/room-24px.svg | 1 + images/textsms-24px.svg | 1 + monitor/filter_c3_exception_decoder.py | 148 + partition-table.csv | 8 + platformio.ini | 166 + pyocd.yaml | 7 + src/AmbientLightingThread.h | 186 + src/AudioThread.h | 77 + src/BluetoothCommon.cpp | 17 + src/BluetoothCommon.h | 32 + src/ButtonThread.cpp | 335 + src/ButtonThread.h | 68 + src/DebugConfiguration.cpp | 198 + src/DebugConfiguration.h | 167 + src/DisplayFormatters.cpp | 37 + src/DisplayFormatters.h | 8 + src/FSCommon.cpp | 377 + src/FSCommon.h | 63 + src/Fusion/Fusion.h | 32 + src/Fusion/FusionAhrs.c | 542 + src/Fusion/FusionAhrs.h | 112 + src/Fusion/FusionAxes.h | 188 + src/Fusion/FusionCalibration.h | 49 + src/Fusion/FusionCompass.c | 51 + src/Fusion/FusionCompass.h | 26 + src/Fusion/FusionConvention.h | 25 + src/Fusion/FusionMath.h | 503 + src/Fusion/FusionOffset.c | 80 + src/Fusion/FusionOffset.h | 40 + src/GPSStatus.h | 141 + src/GpioLogic.cpp | 104 + src/GpioLogic.h | 160 + src/Led.cpp | 66 + src/Led.h | 7 + src/NodeStatus.h | 68 + src/Observer.cpp | 2 + src/Observer.h | 106 + src/Power.cpp | 1147 ++ src/PowerFSM.cpp | 402 + src/PowerFSM.h | 51 + src/PowerFSMThread.h | 45 + src/PowerMon.cpp | 47 + src/PowerMon.h | 44 + src/PowerStatus.h | 103 + src/RF95Configuration.h | 6 + src/RedirectablePrint.cpp | 396 + src/RedirectablePrint.h | 59 + src/SPILock.cpp | 12 + src/SPILock.h | 12 + src/SafeFile.cpp | 105 + src/SafeFile.h | 49 + src/SerialConsole.cpp | 107 + src/SerialConsole.h | 48 + src/Status.h | 56 + src/airtime.cpp | 221 + src/airtime.h | 89 + src/buzz/buzz.cpp | 80 + src/buzz/buzz.h | 7 + src/commands.h | 18 + src/concurrency/BinarySemaphoreFreeRTOS.cpp | 40 + src/concurrency/BinarySemaphoreFreeRTOS.h | 30 + src/concurrency/BinarySemaphorePosix.cpp | 28 + src/concurrency/BinarySemaphorePosix.h | 30 + src/concurrency/InterruptableDelay.cpp | 35 + src/concurrency/InterruptableDelay.h | 41 + src/concurrency/Lock.cpp | 32 + src/concurrency/Lock.h | 35 + src/concurrency/LockGuard.cpp | 17 + src/concurrency/LockGuard.h | 24 + src/concurrency/NotifiedWorkerThread.cpp | 94 + src/concurrency/NotifiedWorkerThread.h | 56 + src/concurrency/OSThread.cpp | 142 + src/concurrency/OSThread.h | 91 + src/concurrency/Periodic.h | 24 + src/configuration.h | 360 + src/detect/LoRaRadioType.h | 17 + src/detect/ScanI2C.cpp | 77 + src/detect/ScanI2C.h | 130 + src/detect/ScanI2CTwoWire.cpp | 493 + src/detect/ScanI2CTwoWire.h | 62 + src/detect/einkScan.h | 67 + src/error.h | 12 + src/freertosinc.h | 47 + src/gps/GPS.cpp | 1713 +++ src/gps/GPS.h | 320 + src/gps/GPSUpdateScheduling.cpp | 118 + src/gps/GPSUpdateScheduling.h | 29 + src/gps/GeoCoord.cpp | 576 + src/gps/GeoCoord.h | 165 + src/gps/NMEAWPL.cpp | 102 + src/gps/NMEAWPL.h | 8 + src/gps/RTC.cpp | 293 + src/gps/RTC.h | 48 + src/gps/cas.h | 63 + src/gps/ubx.h | 478 + src/graphics/EInkDisplay2.cpp | 204 + src/graphics/EInkDisplay2.h | 80 + src/graphics/EInkDynamicDisplay.cpp | 556 + src/graphics/EInkDynamicDisplay.h | 149 + src/graphics/NeoPixel.h | 4 + src/graphics/PointStruct.h | 4 + src/graphics/RAKled.h | 5 + src/graphics/Screen.cpp | 2750 ++++ src/graphics/Screen.h | 603 + src/graphics/ScreenFonts.h | 44 + src/graphics/TFTDisplay.cpp | 863 ++ src/graphics/TFTDisplay.h | 60 + src/graphics/fonts/OLEDDisplayFontsPL.cpp | 440 + src/graphics/fonts/OLEDDisplayFontsPL.h | 11 + src/graphics/fonts/OLEDDisplayFontsRU.cpp | 426 + src/graphics/fonts/OLEDDisplayFontsRU.h | 11 + src/graphics/fonts/OLEDDisplayFontsUA.cpp | 424 + src/graphics/fonts/OLEDDisplayFontsUA.h | 11 + src/graphics/images.h | 207 + src/graphics/img/icon.xbm | 22 + src/input/BBQ10Keyboard.cpp | 181 + src/input/BBQ10Keyboard.h | 51 + src/input/ExpressLRSFiveWay.cpp | 259 + src/input/ExpressLRSFiveWay.h | 85 + src/input/InputBroker.cpp | 18 + src/input/InputBroker.h | 43 + src/input/LinuxInput.cpp | 187 + src/input/LinuxInput.h | 65 + src/input/LinuxInputImpl.cpp | 15 + src/input/LinuxInputImpl.h | 21 + src/input/MPR121Keyboard.cpp | 430 + src/input/MPR121Keyboard.h | 56 + src/input/RotaryEncoderInterruptBase.cpp | 116 + src/input/RotaryEncoderInterruptBase.h | 41 + src/input/RotaryEncoderInterruptImpl1.cpp | 42 + src/input/RotaryEncoderInterruptImpl1.h | 21 + src/input/ScanAndSelect.cpp | 205 + src/input/ScanAndSelect.h | 50 + src/input/SerialKeyboard.cpp | 171 + src/input/SerialKeyboard.h | 25 + src/input/SerialKeyboardImpl.cpp | 21 + src/input/SerialKeyboardImpl.h | 19 + src/input/TouchScreenBase.cpp | 137 + src/input/TouchScreenBase.h | 56 + src/input/TouchScreenImpl1.cpp | 93 + src/input/TouchScreenImpl1.h | 17 + src/input/TrackballInterruptBase.cpp | 95 + src/input/TrackballInterruptBase.h | 44 + src/input/TrackballInterruptImpl1.cpp | 54 + src/input/TrackballInterruptImpl1.h | 16 + src/input/UpDownInterruptBase.cpp | 71 + src/input/UpDownInterruptBase.h | 30 + src/input/UpDownInterruptImpl1.cpp | 41 + src/input/UpDownInterruptImpl1.h | 14 + src/input/cardKbI2cImpl.cpp | 65 + src/input/cardKbI2cImpl.h | 18 + src/input/kbI2cBase.cpp | 401 + src/input/kbI2cBase.h | 25 + src/input/kbMatrixBase.cpp | 131 + src/input/kbMatrixBase.h | 20 + src/input/kbMatrixImpl.cpp | 20 + src/input/kbMatrixImpl.h | 19 + src/main.cpp | 1198 ++ src/main.h | 89 + src/memGet.cpp | 81 + src/memGet.h | 18 + src/mesh/Channels.cpp | 406 + src/mesh/Channels.h | 145 + src/mesh/CryptoEngine.cpp | 252 + src/mesh/CryptoEngine.h | 97 + src/mesh/Default.cpp | 62 + src/mesh/Default.h | 52 + src/mesh/FloodingRouter.cpp | 79 + src/mesh/FloodingRouter.h | 61 + src/mesh/InterfacesTemplates.cpp | 38 + src/mesh/LLCC68Interface.cpp | 11 + src/mesh/LLCC68Interface.h | 19 + src/mesh/LR1110Interface.cpp | 12 + src/mesh/LR1110Interface.h | 14 + src/mesh/LR1120Interface.cpp | 17 + src/mesh/LR1120Interface.h | 15 + src/mesh/LR1121Interface.cpp | 16 + src/mesh/LR1121Interface.h | 16 + src/mesh/LR11x0Interface.cpp | 302 + src/mesh/LR11x0Interface.h | 69 + src/mesh/MemoryPool.h | 75 + src/mesh/MeshModule.cpp | 298 + src/mesh/MeshModule.h | 219 + src/mesh/MeshPacketQueue.cpp | 130 + src/mesh/MeshPacketQueue.h | 40 + src/mesh/MeshRadio.h | 25 + src/mesh/MeshService.cpp | 421 + src/mesh/MeshService.h | 168 + src/mesh/MeshTypes.h | 62 + src/mesh/NodeDB.cpp | 1277 ++ src/mesh/NodeDB.h | 241 + src/mesh/PacketHistory.cpp | 78 + src/mesh/PacketHistory.h | 45 + src/mesh/PhoneAPI.cpp | 639 + src/mesh/PhoneAPI.h | 173 + src/mesh/PointerQueue.h | 30 + src/mesh/ProtobufModule.cpp | 2 + src/mesh/ProtobufModule.h | 123 + src/mesh/RF95Interface.cpp | 340 + src/mesh/RF95Interface.h | 72 + src/mesh/RadioInterface.cpp | 627 + src/mesh/RadioInterface.h | 256 + src/mesh/RadioLibInterface.cpp | 496 + src/mesh/RadioLibInterface.h | 201 + src/mesh/RadioLibRF95.cpp | 86 + src/mesh/RadioLibRF95.h | 73 + src/mesh/ReliableRouter.cpp | 269 + src/mesh/ReliableRouter.h | 123 + src/mesh/Router.cpp | 671 + src/mesh/Router.h | 159 + src/mesh/STM32WLE5JCInterface.cpp | 42 + src/mesh/STM32WLE5JCInterface.h | 31 + src/mesh/SX1262Interface.cpp | 11 + src/mesh/SX1262Interface.h | 15 + src/mesh/SX1268Interface.cpp | 20 + src/mesh/SX1268Interface.h | 17 + src/mesh/SX126xInterface.cpp | 346 + src/mesh/SX126xInterface.h | 72 + src/mesh/SX1280Interface.cpp | 11 + src/mesh/SX1280Interface.h | 15 + src/mesh/SX128xInterface.cpp | 318 + src/mesh/SX128xInterface.h | 70 + src/mesh/SinglePortModule.h | 39 + src/mesh/StreamAPI.cpp | 153 + src/mesh/StreamAPI.h | 88 + src/mesh/Throttle.cpp | 35 + src/mesh/Throttle.h | 10 + src/mesh/TypeConversions.cpp | 106 + src/mesh/TypeConversions.h | 15 + src/mesh/TypedQueue.h | 115 + src/mesh/aes-ccm.cpp | 157 + src/mesh/aes-ccm.h | 10 + src/mesh/api/ServerAPI.cpp | 81 + src/mesh/api/ServerAPI.h | 56 + src/mesh/api/WiFiServerAPI.cpp | 29 + src/mesh/api/WiFiServerAPI.h | 26 + src/mesh/api/ethServerAPI.cpp | 27 + src/mesh/api/ethServerAPI.h | 25 + src/mesh/compression/unishox2.cpp | 1432 ++ src/mesh/compression/unishox2.h | 347 + src/mesh/eth/ethClient.cpp | 190 + src/mesh/eth/ethClient.h | 8 + src/mesh/generated/.clang-format | 2 + src/mesh/generated/meshtastic/admin.pb.cpp | 22 + src/mesh/generated/meshtastic/admin.pb.h | 393 + src/mesh/generated/meshtastic/apponly.pb.cpp | 12 + src/mesh/generated/meshtastic/apponly.pb.h | 64 + src/mesh/generated/meshtastic/atak.pb.cpp | 31 + src/mesh/generated/meshtastic/atak.pb.h | 286 + .../meshtastic/cannedmessages.pb.cpp | 12 + .../generated/meshtastic/cannedmessages.pb.h | 50 + src/mesh/generated/meshtastic/channel.pb.cpp | 20 + src/mesh/generated/meshtastic/channel.pb.h | 198 + .../generated/meshtastic/clientonly.pb.cpp | 12 + src/mesh/generated/meshtastic/clientonly.pb.h | 90 + src/mesh/generated/meshtastic/config.pb.cpp | 68 + src/mesh/generated/meshtastic/config.pb.h | 980 ++ .../meshtastic/connection_status.pb.cpp | 27 + .../meshtastic/connection_status.pb.h | 190 + .../generated/meshtastic/device_ui.pb.cpp | 22 + src/mesh/generated/meshtastic/device_ui.pb.h | 226 + .../generated/meshtastic/deviceonly.pb.cpp | 24 + src/mesh/generated/meshtastic/deviceonly.pb.h | 290 + .../generated/meshtastic/localonly.pb.cpp | 15 + src/mesh/generated/meshtastic/localonly.pb.h | 197 + src/mesh/generated/meshtastic/mesh.pb.cpp | 102 + src/mesh/generated/meshtastic/mesh.pb.h | 1694 +++ .../generated/meshtastic/module_config.pb.cpp | 69 + .../generated/meshtastic/module_config.pb.h | 901 ++ src/mesh/generated/meshtastic/mqtt.pb.cpp | 15 + src/mesh/generated/meshtastic/mqtt.pb.h | 130 + src/mesh/generated/meshtastic/paxcount.pb.cpp | 12 + src/mesh/generated/meshtastic/paxcount.pb.h | 58 + src/mesh/generated/meshtastic/portnums.pb.cpp | 11 + src/mesh/generated/meshtastic/portnums.pb.h | 154 + src/mesh/generated/meshtastic/powermon.pb.cpp | 19 + src/mesh/generated/meshtastic/powermon.pb.h | 138 + .../meshtastic/remote_hardware.pb.cpp | 14 + .../generated/meshtastic/remote_hardware.pb.h | 94 + src/mesh/generated/meshtastic/rtttl.pb.cpp | 12 + src/mesh/generated/meshtastic/rtttl.pb.h | 50 + .../generated/meshtastic/storeforward.pb.cpp | 23 + .../generated/meshtastic/storeforward.pb.h | 220 + .../generated/meshtastic/telemetry.pb.cpp | 35 + src/mesh/generated/meshtastic/telemetry.pb.h | 535 + src/mesh/generated/meshtastic/xmodem.pb.cpp | 14 + src/mesh/generated/meshtastic/xmodem.pb.h | 78 + src/mesh/http/ContentHandler.cpp | 861 ++ src/mesh/http/ContentHandler.h | 36 + src/mesh/http/ContentHelper.cpp | 14 + src/mesh/http/ContentHelper.h | 6 + src/mesh/http/WebServer.cpp | 213 + src/mesh/http/WebServer.h | 22 + src/mesh/mesh-pb-constants.cpp | 74 + src/mesh/mesh-pb-constants.h | 50 + src/mesh/raspihttp/PiWebServer.cpp | 530 + src/mesh/raspihttp/PiWebServer.h | 61 + src/mesh/wifi/WiFiAPClient.cpp | 429 + src/mesh/wifi/WiFiAPClient.h | 22 + src/meshUtils.cpp | 111 + src/meshUtils.h | 29 + src/modules/AdminModule.cpp | 1099 ++ src/modules/AdminModule.h | 65 + src/modules/AtakPluginModule.cpp | 198 + src/modules/AtakPluginModule.h | 26 + src/modules/CannedMessageModule.cpp | 1259 ++ src/modules/CannedMessageModule.h | 227 + src/modules/DetectionSensorModule.cpp | 159 + src/modules/DetectionSensorModule.h | 24 + src/modules/DropzoneModule.cpp | 95 + src/modules/DropzoneModule.h | 37 + src/modules/ExternalNotificationModule.cpp | 603 + src/modules/ExternalNotificationModule.h | 69 + src/modules/ModuleDev.h | 10 + src/modules/Modules.cpp | 248 + src/modules/Modules.h | 6 + src/modules/NeighborInfoModule.cpp | 213 + src/modules/NeighborInfoModule.h | 70 + src/modules/NodeInfoModule.cpp | 109 + src/modules/NodeInfoModule.h | 45 + src/modules/PositionModule.cpp | 498 + src/modules/PositionModule.h | 75 + src/modules/PowerStressModule.cpp | 134 + src/modules/PowerStressModule.h | 38 + src/modules/RangeTestModule.cpp | 295 + src/modules/RangeTestModule.h | 56 + src/modules/RemoteHardwareModule.cpp | 168 + src/modules/RemoteHardwareModule.h | 47 + src/modules/ReplyModule.cpp | 26 + src/modules/ReplyModule.h | 20 + src/modules/RoutingModule.cpp | 85 + src/modules/RoutingModule.h | 39 + src/modules/SerialModule.cpp | 590 + src/modules/SerialModule.h | 80 + src/modules/StoreForwardModule.cpp | 620 + src/modules/StoreForwardModule.h | 108 + src/modules/Telemetry/AirQualityTelemetry.cpp | 193 + src/modules/Telemetry/AirQualityTelemetry.h | 53 + src/modules/Telemetry/DeviceTelemetry.cpp | 175 + src/modules/Telemetry/DeviceTelemetry.h | 63 + .../Telemetry/EnvironmentTelemetry.cpp | 589 + src/modules/Telemetry/EnvironmentTelemetry.h | 63 + src/modules/Telemetry/HealthTelemetry.cpp | 249 + src/modules/Telemetry/HealthTelemetry.h | 60 + src/modules/Telemetry/PowerTelemetry.cpp | 256 + src/modules/Telemetry/PowerTelemetry.h | 59 + src/modules/Telemetry/Sensor/AHT10.cpp | 44 + src/modules/Telemetry/Sensor/AHT10.h | 23 + src/modules/Telemetry/Sensor/BME280Sensor.cpp | 46 + src/modules/Telemetry/Sensor/BME280Sensor.h | 23 + src/modules/Telemetry/Sensor/BME680Sensor.cpp | 148 + src/modules/Telemetry/Sensor/BME680Sensor.h | 46 + src/modules/Telemetry/Sensor/BMP085Sensor.cpp | 39 + src/modules/Telemetry/Sensor/BMP085Sensor.h | 23 + src/modules/Telemetry/Sensor/BMP280Sensor.cpp | 45 + src/modules/Telemetry/Sensor/BMP280Sensor.h | 23 + src/modules/Telemetry/Sensor/BMP3XXSensor.cpp | 89 + src/modules/Telemetry/Sensor/BMP3XXSensor.h | 56 + .../Telemetry/Sensor/DFRobotLarkSensor.cpp | 59 + .../Telemetry/Sensor/DFRobotLarkSensor.h | 29 + src/modules/Telemetry/Sensor/INA219Sensor.cpp | 48 + src/modules/Telemetry/Sensor/INA219Sensor.h | 25 + src/modules/Telemetry/Sensor/INA260Sensor.cpp | 43 + src/modules/Telemetry/Sensor/INA260Sensor.h | 25 + .../Telemetry/Sensor/INA3221Sensor.cpp | 105 + src/modules/Telemetry/Sensor/INA3221Sensor.h | 50 + .../Telemetry/Sensor/LPS22HBSensor.cpp | 43 + src/modules/Telemetry/Sensor/LPS22HBSensor.h | 24 + .../Telemetry/Sensor/MAX17048Sensor.cpp | 176 + src/modules/Telemetry/Sensor/MAX17048Sensor.h | 111 + .../Telemetry/Sensor/MAX30102Sensor.cpp | 83 + src/modules/Telemetry/Sensor/MAX30102Sensor.h | 26 + .../Telemetry/Sensor/MCP9808Sensor.cpp | 36 + src/modules/Telemetry/Sensor/MCP9808Sensor.h | 23 + .../Telemetry/Sensor/MLX90614Sensor.cpp | 44 + src/modules/Telemetry/Sensor/MLX90614Sensor.h | 24 + .../Telemetry/Sensor/MLX90632Sensor.cpp | 41 + src/modules/Telemetry/Sensor/MLX90632Sensor.h | 23 + .../Telemetry/Sensor/NAU7802Sensor.cpp | 140 + src/modules/Telemetry/Sensor/NAU7802Sensor.h | 31 + .../Telemetry/Sensor/OPT3001Sensor.cpp | 50 + src/modules/Telemetry/Sensor/OPT3001Sensor.h | 24 + .../Telemetry/Sensor/RCWL9620Sensor.cpp | 66 + src/modules/Telemetry/Sensor/RCWL9620Sensor.h | 29 + src/modules/Telemetry/Sensor/SHT31Sensor.cpp | 38 + src/modules/Telemetry/Sensor/SHT31Sensor.h | 23 + src/modules/Telemetry/Sensor/SHT4XSensor.cpp | 52 + src/modules/Telemetry/Sensor/SHT4XSensor.h | 23 + src/modules/Telemetry/Sensor/SHTC3Sensor.cpp | 41 + src/modules/Telemetry/Sensor/SHTC3Sensor.h | 23 + src/modules/Telemetry/Sensor/T1000xSensor.cpp | 119 + src/modules/Telemetry/Sensor/T1000xSensor.h | 21 + .../Telemetry/Sensor/TSL2591Sensor.cpp | 44 + src/modules/Telemetry/Sensor/TSL2591Sensor.h | 22 + .../Telemetry/Sensor/TelemetrySensor.cpp | 10 + .../Telemetry/Sensor/TelemetrySensor.h | 61 + .../Telemetry/Sensor/VEML7700Sensor.cpp | 68 + src/modules/Telemetry/Sensor/VEML7700Sensor.h | 27 + src/modules/Telemetry/Sensor/VoltageSensor.h | 13 + src/modules/Telemetry/UnitConversions.cpp | 21 + src/modules/Telemetry/UnitConversions.h | 10 + src/modules/TextMessageModule.cpp | 29 + src/modules/TextMessageModule.h | 26 + src/modules/TraceRouteModule.cpp | 173 + src/modules/TraceRouteModule.h | 36 + src/modules/WaypointModule.cpp | 186 + src/modules/WaypointModule.h | 33 + src/modules/esp32/AudioModule.cpp | 291 + src/modules/esp32/AudioModule.h | 87 + src/modules/esp32/PaxcounterModule.cpp | 130 + src/modules/esp32/PaxcounterModule.h | 38 + src/motion/AccelerometerThread.h | 157 + src/motion/BMA423Sensor.cpp | 62 + src/motion/BMA423Sensor.h | 26 + src/motion/BMX160Sensor.cpp | 104 + src/motion/BMX160Sensor.h | 41 + src/motion/ICM20948Sensor.cpp | 186 + src/motion/ICM20948Sensor.h | 96 + src/motion/LIS3DHSensor.cpp | 37 + src/motion/LIS3DHSensor.h | 24 + src/motion/LSM6DS3Sensor.cpp | 34 + src/motion/LSM6DS3Sensor.h | 28 + src/motion/MPU6050Sensor.cpp | 31 + src/motion/MPU6050Sensor.h | 24 + src/motion/MotionSensor.cpp | 79 + src/motion/MotionSensor.h | 86 + src/motion/QMA6100PSensor.cpp | 183 + src/motion/QMA6100PSensor.h | 63 + src/motion/STK8XXXSensor.cpp | 40 + src/motion/STK8XXXSensor.h | 37 + src/mqtt/MQTT.cpp | 771 + src/mqtt/MQTT.h | 133 + src/network-stubs.cpp | 31 + src/nimble/NimbleBluetooth.cpp | 301 + src/nimble/NimbleBluetooth.h | 22 + src/platform/esp32/BleOta.cpp | 46 + src/platform/esp32/BleOta.h | 20 + src/platform/esp32/ESP32CryptoEngine.cpp | 41 + src/platform/esp32/SimpleAllocator.cpp | 28 + src/platform/esp32/SimpleAllocator.h | 43 + src/platform/esp32/architecture.h | 201 + src/platform/esp32/iram-quirk.c | 23 + src/platform/esp32/main-esp32.cpp | 246 + src/platform/extra_variants/README.md | 15 + .../heltec_wireless_tracker/variant.cpp | 41 + src/platform/nrf52/BLEDfuScure.cpp | 143 + src/platform/nrf52/BLEDfuSecure.h | 55 + src/platform/nrf52/NRF52Bluetooth.cpp | 377 + src/platform/nrf52/NRF52Bluetooth.h | 22 + src/platform/nrf52/NRF52CryptoEngine.cpp | 32 + src/platform/nrf52/aes-256/tiny-aes.cpp | 216 + src/platform/nrf52/aes-256/tiny-aes.h | 22 + src/platform/nrf52/alloc.cpp | 32 + src/platform/nrf52/architecture.h | 128 + src/platform/nrf52/hardfault.cpp | 112 + src/platform/nrf52/main-bare.cpp | 1 + src/platform/nrf52/main-nrf52.cpp | 312 + src/platform/nrf52/nrf52840_s140_v7.ld | 38 + src/platform/nrf52/softdevice/ble.h | 652 + src/platform/nrf52/softdevice/ble_err.h | 92 + src/platform/nrf52/softdevice/ble_gap.h | 2895 ++++ src/platform/nrf52/softdevice/ble_gatt.h | 232 + src/platform/nrf52/softdevice/ble_gattc.h | 764 + src/platform/nrf52/softdevice/ble_gatts.h | 904 ++ src/platform/nrf52/softdevice/ble_hci.h | 135 + src/platform/nrf52/softdevice/ble_l2cap.h | 495 + src/platform/nrf52/softdevice/ble_ranges.h | 149 + src/platform/nrf52/softdevice/ble_types.h | 217 + src/platform/nrf52/softdevice/nrf52/nrf_mbr.h | 259 + src/platform/nrf52/softdevice/nrf_error.h | 90 + src/platform/nrf52/softdevice/nrf_error_sdm.h | 73 + src/platform/nrf52/softdevice/nrf_error_soc.h | 85 + src/platform/nrf52/softdevice/nrf_nvic.h | 449 + src/platform/nrf52/softdevice/nrf_sdm.h | 380 + src/platform/nrf52/softdevice/nrf_soc.h | 1046 ++ src/platform/nrf52/softdevice/nrf_svc.h | 98 + src/platform/portduino/PortduinoGlue.cpp | 420 + src/platform/portduino/PortduinoGlue.h | 70 + src/platform/portduino/SimRadio.cpp | 268 + src/platform/portduino/SimRadio.h | 85 + src/platform/portduino/architecture.h | 19 + src/platform/rp2xx0/architecture.h | 36 + .../hardware_rosc/include/hardware/rosc.h | 93 + src/platform/rp2xx0/hardware_rosc/rosc.c | 70 + src/platform/rp2xx0/main-rp2xx0.cpp | 147 + .../rp2xx0/pico_sleep/include/pico/sleep.h | 107 + src/platform/rp2xx0/pico_sleep/sleep.c | 155 + src/platform/stm32wl/architecture.h | 31 + src/platform/stm32wl/main-stm32wl.cpp | 28 + src/power.h | 102 + src/serialization/JSON.cpp | 245 + src/serialization/JSON.h | 73 + src/serialization/JSONValue.cpp | 890 ++ src/serialization/JSONValue.h | 95 + src/serialization/MeshPacketSerializer.cpp | 358 + src/serialization/MeshPacketSerializer.h | 24 + .../MeshPacketSerializer_nRF52.cpp | 353 + src/shutdown.h | 56 + src/sleep.cpp | 515 + src/sleep.h | 47 + src/target_specific.h | 10 + src/xmodem.cpp | 254 + src/xmodem.h | 80 + suppressions.txt | 56 + test/test_crypto/test_main.cpp | 184 + userPrefs.h | 77 + userPrefs.json | 16 + variants/CDEBYTE_EoRa-S3/pins_arduino.h | 28 + variants/CDEBYTE_EoRa-S3/platformio.ini | 8 + variants/CDEBYTE_EoRa-S3/variant.h | 63 + .../platformio.ini | 14 + .../Dongle_nRF52840-pca10059-v1/variant.cpp | 35 + .../Dongle_nRF52840-pca10059-v1/variant.h | 167 + variants/EBYTE_ESP32-S3/pins_arduino.h | 28 + variants/EBYTE_ESP32-S3/platformio.ini | 9 + variants/EBYTE_ESP32-S3/variant.h | 193 + variants/ME25LS01-4Y10TD/platformio.ini | 15 + variants/ME25LS01-4Y10TD/rfswitch.h | 15 + variants/ME25LS01-4Y10TD/variant.cpp | 40 + variants/ME25LS01-4Y10TD/variant.h | 138 + variants/ME25LS01-4Y10TD_e-ink/platformio.ini | 19 + variants/ME25LS01-4Y10TD_e-ink/rfswitch.h | 15 + variants/ME25LS01-4Y10TD_e-ink/variant.cpp | 40 + variants/ME25LS01-4Y10TD_e-ink/variant.h | 161 + variants/MS24SF1/platformio.ini | 15 + variants/MS24SF1/variant.cpp | 30 + variants/MS24SF1/variant.h | 139 + .../MakePython_nRF52840_eink/platformio.ini | 17 + variants/MakePython_nRF52840_eink/variant.cpp | 38 + variants/MakePython_nRF52840_eink/variant.h | 148 + .../MakePython_nRF52840_oled/platformio.ini | 11 + variants/MakePython_nRF52840_oled/variant.cpp | 38 + variants/MakePython_nRF52840_oled/variant.h | 126 + variants/TWC_mesh_v4/platformio.ini | 10 + variants/TWC_mesh_v4/variant.cpp | 38 + variants/TWC_mesh_v4/variant.h | 133 + variants/ai-c3/platformio.ini | 8 + variants/ai-c3/variant.h | 32 + variants/betafpv_2400_tx_micro/platformio.ini | 18 + variants/betafpv_2400_tx_micro/variant.h | 37 + variants/betafpv_900_tx_nano/platformio.ini | 17 + variants/betafpv_900_tx_nano/variant.h | 28 + variants/bpi_picow_esp32_s3/pins_arduino.h | 29 + variants/bpi_picow_esp32_s3/platformio.ini | 14 + variants/bpi_picow_esp32_s3/variant.h | 74 + variants/canaryone/platformio.ini | 14 + variants/canaryone/variant.cpp | 56 + variants/canaryone/variant.h | 181 + variants/chatter2/platformio.ini | 12 + variants/chatter2/variant.h | 125 + variants/diy/dr-dev/variant.h | 76 + variants/diy/hydra/variant.h | 41 + variants/diy/mesh-tab/variant.h | 49 + .../diy/nrf52_promicro_diy_tcxo/variant.cpp | 38 + .../diy/nrf52_promicro_diy_tcxo/variant.h | 182 + .../diy/nrf52_promicro_diy_xtal/variant.cpp | 38 + .../diy/nrf52_promicro_diy_xtal/variant.h | 155 + variants/diy/platformio.ini | 160 + variants/diy/t-energy-s3_e22/variant.h | 46 + variants/diy/v1/variant.h | 56 + variants/diy/v1_1/variant.h | 57 + variants/dreamcatcher/platformio.ini | 27 + variants/dreamcatcher/rfswitch.h | 17 + variants/dreamcatcher/variant.h | 109 + variants/esp32-s3-pico/pins_arduino.h | 28 + variants/esp32-s3-pico/platformio.ini | 25 + variants/esp32-s3-pico/variant.h | 81 + variants/feather_diy/platformio.ini | 12 + variants/feather_diy/variant.cpp | 24 + variants/feather_diy/variant.h | 119 + variants/feather_rp2040_rfm95/platformio.ini | 16 + variants/feather_rp2040_rfm95/variant.h | 61 + .../heltec_capsule_sensor_v3/platformio.ini | 11 + variants/heltec_capsule_sensor_v3/variant.h | 53 + variants/heltec_esp32c3/pins_arduino.h | 24 + variants/heltec_esp32c3/platformio.ini | 11 + variants/heltec_esp32c3/variant.h | 29 + variants/heltec_hru_3601/pins_arduino.h | 25 + variants/heltec_hru_3601/platformio.ini | 9 + variants/heltec_hru_3601/variant.h | 40 + variants/heltec_mesh_node_t114/platformio.ini | 17 + variants/heltec_mesh_node_t114/variant.cpp | 38 + variants/heltec_mesh_node_t114/variant.h | 218 + variants/heltec_v1/platformio.ini | 7 + variants/heltec_v1/variant.h | 31 + variants/heltec_v2.1/platformio.ini | 8 + variants/heltec_v2.1/variant.h | 37 + variants/heltec_v2/platformio.ini | 7 + variants/heltec_v2/variant.h | 31 + variants/heltec_v3/platformio.ini | 8 + variants/heltec_v3/variant.h | 42 + .../heltec_vision_master_e213/pins_arduino.h | 61 + .../heltec_vision_master_e213/platformio.ini | 23 + variants/heltec_vision_master_e213/variant.h | 56 + .../heltec_vision_master_e290/pins_arduino.h | 61 + .../heltec_vision_master_e290/platformio.ini | 25 + variants/heltec_vision_master_e290/variant.h | 55 + .../heltec_vision_master_t190/pins_arduino.h | 61 + .../heltec_vision_master_t190/platformio.ini | 13 + variants/heltec_vision_master_t190/variant.h | 72 + .../heltec_wireless_bridge/platformio.ini | 6 + variants/heltec_wireless_bridge/variant.h | 29 + variants/heltec_wireless_paper/pins_arduino.h | 61 + variants/heltec_wireless_paper/platformio.ini | 23 + variants/heltec_wireless_paper/variant.h | 54 + .../heltec_wireless_paper_v1/pins_arduino.h | 66 + .../heltec_wireless_paper_v1/platformio.ini | 22 + variants/heltec_wireless_paper_v1/variant.h | 54 + .../heltec_wireless_tracker/pins_arduino.h | 72 + .../heltec_wireless_tracker/platformio.ini | 14 + variants/heltec_wireless_tracker/variant.h | 78 + .../pins_arduino.h | 72 + .../platformio.ini | 14 + .../heltec_wireless_tracker_V1_0/variant.h | 72 + variants/heltec_wsl_v3/platformio.ini | 7 + variants/heltec_wsl_v3/variant.h | 36 + variants/icarus/pins_arduino.h | 22 + variants/icarus/platformio.ini | 19 + variants/icarus/variant.h | 31 + variants/m5stack-stamp-c3/pins_arduino.h | 24 + variants/m5stack-stamp-c3/platformio.ini | 12 + variants/m5stack-stamp-c3/variant.h | 73 + variants/m5stack_core/pins_arduino.h | 47 + variants/m5stack_core/platformio.ini | 28 + variants/m5stack_core/variant.h | 46 + variants/m5stack_coreink/pins_arduino.h | 49 + variants/m5stack_coreink/platformio.ini | 27 + variants/m5stack_coreink/variant.h | 109 + variants/m5stack_cores3/pins_arduino.h | 63 + variants/m5stack_cores3/platformio.ini | 14 + variants/m5stack_cores3/variant.h | 22 + variants/monteops_hw1/platformio.ini | 15 + variants/monteops_hw1/variant.cpp | 41 + variants/monteops_hw1/variant.h | 233 + variants/my_esp32s3_diy_eink/pins_arduino.h | 26 + variants/my_esp32s3_diy_eink/platformio.ini | 27 + variants/my_esp32s3_diy_eink/variant.h | 60 + variants/my_esp32s3_diy_oled/pins_arduino.h | 26 + variants/my_esp32s3_diy_oled/platformio.ini | 22 + variants/my_esp32s3_diy_oled/variant.h | 60 + variants/nano-g1-explorer/platformio.ini | 8 + variants/nano-g1-explorer/variant.h | 42 + variants/nano-g1/platformio.ini | 8 + variants/nano-g1/variant.h | 38 + variants/nano-g2-ultra/platformio.ini | 13 + variants/nano-g2-ultra/variant.cpp | 36 + variants/nano-g2-ultra/variant.h | 188 + variants/picomputer-s3/pins_arduino.h | 22 + variants/picomputer-s3/platformio.ini | 17 + variants/picomputer-s3/variant.h | 65 + variants/portduino/platformio.ini | 10 + variants/portduino/variant.h | 8 + .../radiomaster_900_bandit/platformio.ini | 14 + variants/radiomaster_900_bandit/variant.h | 126 + .../platformio.ini | 19 + .../platformio.ini | 14 + .../radiomaster_900_bandit_nano/variant.h | 81 + variants/rak10701/platformio.ini | 24 + variants/rak10701/variant.cpp | 45 + variants/rak10701/variant.h | 319 + variants/rak11200/pins_arduino.h | 38 + variants/rak11200/platformio.ini | 7 + variants/rak11200/variant.h | 81 + variants/rak11310/pins_arduino.h | 68 + variants/rak11310/platformio.ini | 17 + variants/rak11310/variant.h | 51 + variants/rak2560/RAK9154Sensor.cpp | 183 + variants/rak2560/RAK9154Sensor.h | 23 + variants/rak2560/create_uf2.py | 113 + variants/rak2560/platformio.ini | 22 + variants/rak2560/variant.cpp | 45 + variants/rak2560/variant.h | 281 + variants/rak3172/platformio.ini | 34 + variants/rak3172/variant.h | 12 + variants/rak4631/platformio.ini | 55 + variants/rak4631/variant.cpp | 45 + variants/rak4631/variant.h | 273 + variants/rak4631_epaper/platformio.ini | 22 + variants/rak4631_epaper/variant.cpp | 45 + variants/rak4631_epaper/variant.h | 234 + variants/rak4631_epaper_onrxtx/platformio.ini | 25 + variants/rak4631_epaper_onrxtx/variant.cpp | 45 + variants/rak4631_epaper_onrxtx/variant.h | 205 + variants/rak4631_eth_gw/platformio.ini | 66 + variants/rak4631_eth_gw/variant.cpp | 45 + variants/rak4631_eth_gw/variant.h | 273 + variants/rp2040-lora/platformio.ini | 16 + variants/rp2040-lora/variant.h | 60 + variants/rpipico-slowclock/platformio.ini | 29 + variants/rpipico-slowclock/variant.h | 87 + variants/rpipico/platformio.ini | 16 + variants/rpipico/variant.h | 50 + variants/rpipico2/platformio.ini | 16 + variants/rpipico2/variant.h | 50 + variants/rpipicow/platformio.ini | 18 + variants/rpipicow/variant.h | 54 + .../seeed-sensecap-indicator/pins_arduino.h | 56 + .../seeed-sensecap-indicator/platformio.ini | 28 + variants/seeed-sensecap-indicator/variant.h | 64 + variants/seeed_xiao_s3/pins_arduino.h | 21 + variants/seeed_xiao_s3/platformio.ini | 17 + variants/seeed_xiao_s3/variant.h | 84 + variants/senselora_rp2040/pins_arduino.h | 50 + variants/senselora_rp2040/platformio.ini | 14 + variants/senselora_rp2040/variant.h | 27 + variants/station-g1/platformio.ini | 8 + variants/station-g1/variant.h | 48 + variants/station-g2/pins_arduino.h | 21 + variants/station-g2/platformio.ini | 18 + variants/station-g2/variant.h | 54 + variants/t-deck/pins_arduino.h | 61 + variants/t-deck/platformio.ini | 19 + variants/t-deck/variant.h | 100 + variants/t-echo/platformio.ini | 27 + variants/t-echo/variant.cpp | 44 + variants/t-echo/variant.h | 229 + variants/t-watch-s3/pins_arduino.h | 56 + variants/t-watch-s3/platformio.ini | 18 + variants/t-watch-s3/variant.h | 73 + variants/tbeam-s3-core/pins_arduino.h | 42 + variants/tbeam-s3-core/platformio.ini | 14 + variants/tbeam-s3-core/variant.h | 69 + variants/tbeam/platformio.ini | 11 + variants/tbeam/variant.h | 45 + variants/tbeam_v07/platformio.ini | 7 + variants/tbeam_v07/variant.h | 22 + variants/tlora_c6/platformio.ini | 10 + variants/tlora_c6/variant.h | 21 + variants/tlora_t3s3_epaper/pins_arduino.h | 26 + variants/tlora_t3s3_epaper/platformio.ini | 23 + variants/tlora_t3s3_epaper/variant.h | 69 + variants/tlora_t3s3_v1/pins_arduino.h | 26 + variants/tlora_t3s3_v1/platformio.ini | 9 + variants/tlora_t3s3_v1/rfswitch.h | 11 + variants/tlora_t3s3_v1/variant.h | 79 + variants/tlora_v1/platformio.ini | 6 + variants/tlora_v1/variant.h | 17 + variants/tlora_v1_3/platformio.ini | 6 + variants/tlora_v1_3/variant.h | 18 + variants/tlora_v2/platformio.ini | 6 + variants/tlora_v2/variant.h | 18 + variants/tlora_v2_1_16/platformio.ini | 7 + variants/tlora_v2_1_16/variant.h | 28 + variants/tlora_v2_1_16_tcxo/platformio.ini | 9 + variants/tlora_v2_1_18/platformio.ini | 6 + variants/tlora_v2_1_18/variant.h | 20 + variants/tracker-t1000-e/platformio.ini | 14 + variants/tracker-t1000-e/rfswitch.h | 13 + variants/tracker-t1000-e/variant.cpp | 64 + variants/tracker-t1000-e/variant.h | 162 + variants/trackerd/platformio.ini | 13 + variants/trackerd/variant.h | 53 + variants/tracksenger/internal/pins_arduino.h | 72 + variants/tracksenger/internal/variant.h | 92 + variants/tracksenger/lcd/pins_arduino.h | 72 + variants/tracksenger/lcd/variant.h | 116 + variants/tracksenger/oled/pins_arduino.h | 72 + variants/tracksenger/oled/variant.h | 94 + variants/tracksenger/platformio.ini | 40 + variants/unphone/pins_arduino.h | 67 + variants/unphone/platformio.ini | 77 + variants/unphone/variant.cpp | 21 + variants/unphone/variant.h | 71 + variants/wio-e5/platformio.ini | 36 + variants/wio-e5/variant.h | 21 + variants/wio-sdk-wm1110/platformio.ini | 25 + variants/wio-sdk-wm1110/rfswitch.h | 15 + variants/wio-sdk-wm1110/variant.cpp | 45 + variants/wio-sdk-wm1110/variant.h | 120 + variants/wio-t1000-s/platformio.ini | 15 + variants/wio-t1000-s/rfswitch.h | 13 + variants/wio-t1000-s/variant.cpp | 64 + variants/wio-t1000-s/variant.h | 160 + variants/wio-tracker-wm1110/platformio.ini | 13 + variants/wio-tracker-wm1110/rfswitch.h | 15 + variants/wio-tracker-wm1110/variant.cpp | 45 + variants/wio-tracker-wm1110/variant.h | 114 + variants/wiphone/pins_arduino.h | 44 + variants/wiphone/platformio.ini | 13 + variants/wiphone/variant.h | 62 + variants/xiao_ble/README.md | 261 + variants/xiao_ble/platformio.ini | 14 + variants/xiao_ble/variant.cpp | 55 + variants/xiao_ble/variant.h | 212 + .../xiao_ble/xiao-ble-internal-format.uf2 | Bin 0 -> 122880 bytes variants/xiao_ble/xiao_ble.sh | 15 + ...ootloader-0.7.0-22-g277a0c8_s140_7.3.0.zip | Bin 0 -> 192586 bytes version.properties | 4 + 909 files changed, 132802 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .gitpod.yml create mode 100644 .semgrepignore create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 SECURITY.md create mode 100644 arch/esp32/esp32.ini create mode 100644 arch/esp32/esp32c3.ini create mode 100644 arch/esp32/esp32c6.ini create mode 100644 arch/esp32/esp32s2.ini create mode 100644 arch/esp32/esp32s3.ini create mode 100644 arch/nrf52/cpp_overrides/lfs_util.h create mode 100644 arch/nrf52/nrf52.ini create mode 100644 arch/nrf52/nrf52832.ini create mode 100644 arch/nrf52/nrf52840.ini create mode 100644 arch/portduino/portduino.ini create mode 100644 arch/rp2xx0/rp2040.ini create mode 100644 arch/rp2xx0/rp2350.ini create mode 100644 arch/stm32/stm32.ini create mode 100644 bin/Meshtastic_nRF52_factory_erase_v3_S140_6.1.0.uf2 create mode 100644 bin/Meshtastic_nRF52_factory_erase_v3_S140_7.3.0.uf2 create mode 100644 bin/build-esp32.sh create mode 100644 bin/build-native.sh create mode 100644 bin/build-nrf52.sh create mode 100644 bin/build-rpi2040.sh create mode 100644 bin/build-stm32.sh create mode 100644 bin/build-userprefs-json.py create mode 100644 bin/buildinfo.py create mode 100644 bin/bump_version.py create mode 100644 bin/check-all.sh create mode 100644 bin/check-dependencies.sh create mode 100644 bin/config-dist.yaml create mode 100644 bin/config.d/display-waveshare-2.8.yaml create mode 100644 bin/config.d/lora-waveshare-sxxx.yaml create mode 100644 bin/device-install.bat create mode 100644 bin/device-install.sh create mode 100644 bin/device-update.bat create mode 100644 bin/device-update.sh create mode 100644 bin/dump-ram-users.sh create mode 100644 bin/exception_decoder.py create mode 100644 bin/gen-images.sh create mode 100644 bin/generate_ci_matrix.py create mode 100644 bin/generic/Meshtastic_6.1.0_bootloader-0.9.2_s140_6.1.1.hex create mode 100644 bin/generic/Meshtastic_6.1.0_bootloader-0.9.2_s140_6.1.1.zip create mode 100644 bin/generic/Meshtastic_7.3.0_bootloader-0.9.2_s140_7.3.0.hex create mode 100644 bin/generic/Meshtastic_7.3.0_bootloader-0.9.2_s140_7.3.0.zip create mode 100644 bin/generic/update-Meshtastic_6.1.0_bootloader-0.9.2_nosd.uf2 create mode 100644 bin/generic/update-Meshtastic_7.3.0_bootloader-0.9.2_nosd.uf2 create mode 100644 bin/genpartitions.py create mode 100644 bin/lilygo_techo_bootloader-0.6.1.zip create mode 100644 bin/mergehex create mode 100644 bin/meshtasticd.service create mode 100644 bin/native-gdbserver.sh create mode 100644 bin/native-install.sh create mode 100644 bin/native-run.sh create mode 100644 bin/platformio-custom.py create mode 100644 bin/promote-release.sh create mode 100644 bin/read-system-info.sh create mode 100644 bin/readprops.py create mode 100644 bin/regen-protos.bat create mode 100644 bin/regen-protos.sh create mode 100644 bin/s140_nrf52_7.3.0_softdevice.hex create mode 100644 bin/setup-python-for-esp-debug.sh create mode 100644 bin/test-simulator.sh create mode 100644 bin/uf2-convert.bat create mode 100644 bin/uf2conv.py create mode 100644 bin/update-lilygo_techo_bootloader-0.6.1_nosd.uf2 create mode 100644 bin/view-map.sh create mode 100644 bin/wio_tracker_bootloader_update.bin create mode 100644 boards/CDEBYTE_EoRa-S3.json create mode 100644 boards/ESP32-S3-WROOM-1-N4.json create mode 100644 boards/bpi_picow_esp32_s3.json create mode 100644 boards/canaryone.json create mode 100644 boards/eink0.1.json create mode 100644 boards/esp32-s3-pico.json create mode 100644 boards/heltec_mesh_node_t114.json create mode 100644 boards/heltec_vision_master_e213.json create mode 100644 boards/heltec_vision_master_e290.json create mode 100644 boards/heltec_vision_master_t190.json create mode 100644 boards/heltec_wireless_tracker.json create mode 100644 boards/icarus.json create mode 100644 boards/me25ls01-4y10td.json create mode 100644 boards/ms24sf1.json create mode 100644 boards/my-esp32s3-diy-oled.json create mode 100644 boards/my_esp32s3_diy_eink.json create mode 100644 boards/nano-g2-ultra.json create mode 100644 boards/nordic_pca10059.json create mode 100644 boards/nrf52840_dk.json create mode 100644 boards/promicro-nrf52840.json create mode 100644 boards/seeed-sensecap-indicator.json create mode 100644 boards/seeed-xiao-s3.json create mode 100644 boards/station-g2.json create mode 100644 boards/t-deck.json create mode 100644 boards/t-echo.json create mode 100644 boards/t-watch-s3.json create mode 100644 boards/tbeam-s3-core.json create mode 100644 boards/tlora-t3s3-v1.json create mode 100644 boards/tracker-t1000-e.json create mode 100644 boards/unphone.json create mode 100644 boards/wio-sdk-wm1110.json create mode 100644 boards/wio-t1000-s.json create mode 100644 boards/wio-tracker-wm1110.json create mode 100644 boards/wiphone.json create mode 100644 boards/wiscore_rak11200.json create mode 100644 boards/wiscore_rak11300.json create mode 100644 boards/wiscore_rak3172.json create mode 100644 boards/wiscore_rak4600.json create mode 100644 boards/wiscore_rak4631.json create mode 100644 boards/xiao_ble_sense.json create mode 100644 data/static/.gitkeep create mode 100644 docker-compose.yml create mode 100644 extra_scripts/README.md create mode 100644 extra_scripts/disable_adafruit_usb.py create mode 100644 images/compass.png create mode 100644 images/face-24px.svg create mode 100644 images/face.png create mode 100644 images/location_searching-24px.svg create mode 100644 images/pin.png create mode 100644 images/room-24px.svg create mode 100644 images/textsms-24px.svg create mode 100644 monitor/filter_c3_exception_decoder.py create mode 100644 partition-table.csv create mode 100644 platformio.ini create mode 100644 pyocd.yaml create mode 100644 src/AmbientLightingThread.h create mode 100644 src/AudioThread.h create mode 100644 src/BluetoothCommon.cpp create mode 100644 src/BluetoothCommon.h create mode 100644 src/ButtonThread.cpp create mode 100644 src/ButtonThread.h create mode 100644 src/DebugConfiguration.cpp create mode 100644 src/DebugConfiguration.h create mode 100644 src/DisplayFormatters.cpp create mode 100644 src/DisplayFormatters.h create mode 100644 src/FSCommon.cpp create mode 100644 src/FSCommon.h create mode 100644 src/Fusion/Fusion.h create mode 100644 src/Fusion/FusionAhrs.c create mode 100644 src/Fusion/FusionAhrs.h create mode 100644 src/Fusion/FusionAxes.h create mode 100644 src/Fusion/FusionCalibration.h create mode 100644 src/Fusion/FusionCompass.c create mode 100644 src/Fusion/FusionCompass.h create mode 100644 src/Fusion/FusionConvention.h create mode 100644 src/Fusion/FusionMath.h create mode 100644 src/Fusion/FusionOffset.c create mode 100644 src/Fusion/FusionOffset.h create mode 100644 src/GPSStatus.h create mode 100644 src/GpioLogic.cpp create mode 100644 src/GpioLogic.h create mode 100644 src/Led.cpp create mode 100644 src/Led.h create mode 100644 src/NodeStatus.h create mode 100644 src/Observer.cpp create mode 100644 src/Observer.h create mode 100644 src/Power.cpp create mode 100644 src/PowerFSM.cpp create mode 100644 src/PowerFSM.h create mode 100644 src/PowerFSMThread.h create mode 100644 src/PowerMon.cpp create mode 100644 src/PowerMon.h create mode 100644 src/PowerStatus.h create mode 100644 src/RF95Configuration.h create mode 100644 src/RedirectablePrint.cpp create mode 100644 src/RedirectablePrint.h create mode 100644 src/SPILock.cpp create mode 100644 src/SPILock.h create mode 100644 src/SafeFile.cpp create mode 100644 src/SafeFile.h create mode 100644 src/SerialConsole.cpp create mode 100644 src/SerialConsole.h create mode 100644 src/Status.h create mode 100644 src/airtime.cpp create mode 100644 src/airtime.h create mode 100644 src/buzz/buzz.cpp create mode 100644 src/buzz/buzz.h create mode 100644 src/commands.h create mode 100644 src/concurrency/BinarySemaphoreFreeRTOS.cpp create mode 100644 src/concurrency/BinarySemaphoreFreeRTOS.h create mode 100644 src/concurrency/BinarySemaphorePosix.cpp create mode 100644 src/concurrency/BinarySemaphorePosix.h create mode 100644 src/concurrency/InterruptableDelay.cpp create mode 100644 src/concurrency/InterruptableDelay.h create mode 100644 src/concurrency/Lock.cpp create mode 100644 src/concurrency/Lock.h create mode 100644 src/concurrency/LockGuard.cpp create mode 100644 src/concurrency/LockGuard.h create mode 100644 src/concurrency/NotifiedWorkerThread.cpp create mode 100644 src/concurrency/NotifiedWorkerThread.h create mode 100644 src/concurrency/OSThread.cpp create mode 100644 src/concurrency/OSThread.h create mode 100644 src/concurrency/Periodic.h create mode 100644 src/configuration.h create mode 100644 src/detect/LoRaRadioType.h create mode 100644 src/detect/ScanI2C.cpp create mode 100644 src/detect/ScanI2C.h create mode 100644 src/detect/ScanI2CTwoWire.cpp create mode 100644 src/detect/ScanI2CTwoWire.h create mode 100644 src/detect/einkScan.h create mode 100644 src/error.h create mode 100644 src/freertosinc.h create mode 100644 src/gps/GPS.cpp create mode 100644 src/gps/GPS.h create mode 100644 src/gps/GPSUpdateScheduling.cpp create mode 100644 src/gps/GPSUpdateScheduling.h create mode 100644 src/gps/GeoCoord.cpp create mode 100644 src/gps/GeoCoord.h create mode 100644 src/gps/NMEAWPL.cpp create mode 100644 src/gps/NMEAWPL.h create mode 100644 src/gps/RTC.cpp create mode 100644 src/gps/RTC.h create mode 100644 src/gps/cas.h create mode 100644 src/gps/ubx.h create mode 100644 src/graphics/EInkDisplay2.cpp create mode 100644 src/graphics/EInkDisplay2.h create mode 100644 src/graphics/EInkDynamicDisplay.cpp create mode 100644 src/graphics/EInkDynamicDisplay.h create mode 100644 src/graphics/NeoPixel.h create mode 100644 src/graphics/PointStruct.h create mode 100644 src/graphics/RAKled.h create mode 100644 src/graphics/Screen.cpp create mode 100644 src/graphics/Screen.h create mode 100644 src/graphics/ScreenFonts.h create mode 100644 src/graphics/TFTDisplay.cpp create mode 100644 src/graphics/TFTDisplay.h create mode 100644 src/graphics/fonts/OLEDDisplayFontsPL.cpp create mode 100644 src/graphics/fonts/OLEDDisplayFontsPL.h create mode 100644 src/graphics/fonts/OLEDDisplayFontsRU.cpp create mode 100644 src/graphics/fonts/OLEDDisplayFontsRU.h create mode 100644 src/graphics/fonts/OLEDDisplayFontsUA.cpp create mode 100644 src/graphics/fonts/OLEDDisplayFontsUA.h create mode 100644 src/graphics/images.h create mode 100644 src/graphics/img/icon.xbm create mode 100644 src/input/BBQ10Keyboard.cpp create mode 100644 src/input/BBQ10Keyboard.h create mode 100644 src/input/ExpressLRSFiveWay.cpp create mode 100644 src/input/ExpressLRSFiveWay.h create mode 100644 src/input/InputBroker.cpp create mode 100644 src/input/InputBroker.h create mode 100644 src/input/LinuxInput.cpp create mode 100644 src/input/LinuxInput.h create mode 100644 src/input/LinuxInputImpl.cpp create mode 100644 src/input/LinuxInputImpl.h create mode 100644 src/input/MPR121Keyboard.cpp create mode 100644 src/input/MPR121Keyboard.h create mode 100644 src/input/RotaryEncoderInterruptBase.cpp create mode 100644 src/input/RotaryEncoderInterruptBase.h create mode 100644 src/input/RotaryEncoderInterruptImpl1.cpp create mode 100644 src/input/RotaryEncoderInterruptImpl1.h create mode 100644 src/input/ScanAndSelect.cpp create mode 100644 src/input/ScanAndSelect.h create mode 100644 src/input/SerialKeyboard.cpp create mode 100644 src/input/SerialKeyboard.h create mode 100644 src/input/SerialKeyboardImpl.cpp create mode 100644 src/input/SerialKeyboardImpl.h create mode 100644 src/input/TouchScreenBase.cpp create mode 100644 src/input/TouchScreenBase.h create mode 100644 src/input/TouchScreenImpl1.cpp create mode 100644 src/input/TouchScreenImpl1.h create mode 100644 src/input/TrackballInterruptBase.cpp create mode 100644 src/input/TrackballInterruptBase.h create mode 100644 src/input/TrackballInterruptImpl1.cpp create mode 100644 src/input/TrackballInterruptImpl1.h create mode 100644 src/input/UpDownInterruptBase.cpp create mode 100644 src/input/UpDownInterruptBase.h create mode 100644 src/input/UpDownInterruptImpl1.cpp create mode 100644 src/input/UpDownInterruptImpl1.h create mode 100644 src/input/cardKbI2cImpl.cpp create mode 100644 src/input/cardKbI2cImpl.h create mode 100644 src/input/kbI2cBase.cpp create mode 100644 src/input/kbI2cBase.h create mode 100644 src/input/kbMatrixBase.cpp create mode 100644 src/input/kbMatrixBase.h create mode 100644 src/input/kbMatrixImpl.cpp create mode 100644 src/input/kbMatrixImpl.h create mode 100644 src/main.cpp create mode 100644 src/main.h create mode 100644 src/memGet.cpp create mode 100644 src/memGet.h create mode 100644 src/mesh/Channels.cpp create mode 100644 src/mesh/Channels.h create mode 100644 src/mesh/CryptoEngine.cpp create mode 100644 src/mesh/CryptoEngine.h create mode 100644 src/mesh/Default.cpp create mode 100644 src/mesh/Default.h create mode 100644 src/mesh/FloodingRouter.cpp create mode 100644 src/mesh/FloodingRouter.h create mode 100644 src/mesh/InterfacesTemplates.cpp create mode 100644 src/mesh/LLCC68Interface.cpp create mode 100644 src/mesh/LLCC68Interface.h create mode 100644 src/mesh/LR1110Interface.cpp create mode 100644 src/mesh/LR1110Interface.h create mode 100644 src/mesh/LR1120Interface.cpp create mode 100644 src/mesh/LR1120Interface.h create mode 100644 src/mesh/LR1121Interface.cpp create mode 100644 src/mesh/LR1121Interface.h create mode 100644 src/mesh/LR11x0Interface.cpp create mode 100644 src/mesh/LR11x0Interface.h create mode 100644 src/mesh/MemoryPool.h create mode 100644 src/mesh/MeshModule.cpp create mode 100644 src/mesh/MeshModule.h create mode 100644 src/mesh/MeshPacketQueue.cpp create mode 100644 src/mesh/MeshPacketQueue.h create mode 100644 src/mesh/MeshRadio.h create mode 100644 src/mesh/MeshService.cpp create mode 100644 src/mesh/MeshService.h create mode 100644 src/mesh/MeshTypes.h create mode 100644 src/mesh/NodeDB.cpp create mode 100644 src/mesh/NodeDB.h create mode 100644 src/mesh/PacketHistory.cpp create mode 100644 src/mesh/PacketHistory.h create mode 100644 src/mesh/PhoneAPI.cpp create mode 100644 src/mesh/PhoneAPI.h create mode 100644 src/mesh/PointerQueue.h create mode 100644 src/mesh/ProtobufModule.cpp create mode 100644 src/mesh/ProtobufModule.h create mode 100644 src/mesh/RF95Interface.cpp create mode 100644 src/mesh/RF95Interface.h create mode 100644 src/mesh/RadioInterface.cpp create mode 100644 src/mesh/RadioInterface.h create mode 100644 src/mesh/RadioLibInterface.cpp create mode 100644 src/mesh/RadioLibInterface.h create mode 100644 src/mesh/RadioLibRF95.cpp create mode 100644 src/mesh/RadioLibRF95.h create mode 100644 src/mesh/ReliableRouter.cpp create mode 100644 src/mesh/ReliableRouter.h create mode 100644 src/mesh/Router.cpp create mode 100644 src/mesh/Router.h create mode 100644 src/mesh/STM32WLE5JCInterface.cpp create mode 100644 src/mesh/STM32WLE5JCInterface.h create mode 100644 src/mesh/SX1262Interface.cpp create mode 100644 src/mesh/SX1262Interface.h create mode 100644 src/mesh/SX1268Interface.cpp create mode 100644 src/mesh/SX1268Interface.h create mode 100644 src/mesh/SX126xInterface.cpp create mode 100644 src/mesh/SX126xInterface.h create mode 100644 src/mesh/SX1280Interface.cpp create mode 100644 src/mesh/SX1280Interface.h create mode 100644 src/mesh/SX128xInterface.cpp create mode 100644 src/mesh/SX128xInterface.h create mode 100644 src/mesh/SinglePortModule.h create mode 100644 src/mesh/StreamAPI.cpp create mode 100644 src/mesh/StreamAPI.h create mode 100644 src/mesh/Throttle.cpp create mode 100644 src/mesh/Throttle.h create mode 100644 src/mesh/TypeConversions.cpp create mode 100644 src/mesh/TypeConversions.h create mode 100644 src/mesh/TypedQueue.h create mode 100644 src/mesh/aes-ccm.cpp create mode 100644 src/mesh/aes-ccm.h create mode 100644 src/mesh/api/ServerAPI.cpp create mode 100644 src/mesh/api/ServerAPI.h create mode 100644 src/mesh/api/WiFiServerAPI.cpp create mode 100644 src/mesh/api/WiFiServerAPI.h create mode 100644 src/mesh/api/ethServerAPI.cpp create mode 100644 src/mesh/api/ethServerAPI.h create mode 100644 src/mesh/compression/unishox2.cpp create mode 100644 src/mesh/compression/unishox2.h create mode 100644 src/mesh/eth/ethClient.cpp create mode 100644 src/mesh/eth/ethClient.h create mode 100644 src/mesh/generated/.clang-format create mode 100644 src/mesh/generated/meshtastic/admin.pb.cpp create mode 100644 src/mesh/generated/meshtastic/admin.pb.h create mode 100644 src/mesh/generated/meshtastic/apponly.pb.cpp create mode 100644 src/mesh/generated/meshtastic/apponly.pb.h create mode 100644 src/mesh/generated/meshtastic/atak.pb.cpp create mode 100644 src/mesh/generated/meshtastic/atak.pb.h create mode 100644 src/mesh/generated/meshtastic/cannedmessages.pb.cpp create mode 100644 src/mesh/generated/meshtastic/cannedmessages.pb.h create mode 100644 src/mesh/generated/meshtastic/channel.pb.cpp create mode 100644 src/mesh/generated/meshtastic/channel.pb.h create mode 100644 src/mesh/generated/meshtastic/clientonly.pb.cpp create mode 100644 src/mesh/generated/meshtastic/clientonly.pb.h create mode 100644 src/mesh/generated/meshtastic/config.pb.cpp create mode 100644 src/mesh/generated/meshtastic/config.pb.h create mode 100644 src/mesh/generated/meshtastic/connection_status.pb.cpp create mode 100644 src/mesh/generated/meshtastic/connection_status.pb.h create mode 100644 src/mesh/generated/meshtastic/device_ui.pb.cpp create mode 100644 src/mesh/generated/meshtastic/device_ui.pb.h create mode 100644 src/mesh/generated/meshtastic/deviceonly.pb.cpp create mode 100644 src/mesh/generated/meshtastic/deviceonly.pb.h create mode 100644 src/mesh/generated/meshtastic/localonly.pb.cpp create mode 100644 src/mesh/generated/meshtastic/localonly.pb.h create mode 100644 src/mesh/generated/meshtastic/mesh.pb.cpp create mode 100644 src/mesh/generated/meshtastic/mesh.pb.h create mode 100644 src/mesh/generated/meshtastic/module_config.pb.cpp create mode 100644 src/mesh/generated/meshtastic/module_config.pb.h create mode 100644 src/mesh/generated/meshtastic/mqtt.pb.cpp create mode 100644 src/mesh/generated/meshtastic/mqtt.pb.h create mode 100644 src/mesh/generated/meshtastic/paxcount.pb.cpp create mode 100644 src/mesh/generated/meshtastic/paxcount.pb.h create mode 100644 src/mesh/generated/meshtastic/portnums.pb.cpp create mode 100644 src/mesh/generated/meshtastic/portnums.pb.h create mode 100644 src/mesh/generated/meshtastic/powermon.pb.cpp create mode 100644 src/mesh/generated/meshtastic/powermon.pb.h create mode 100644 src/mesh/generated/meshtastic/remote_hardware.pb.cpp create mode 100644 src/mesh/generated/meshtastic/remote_hardware.pb.h create mode 100644 src/mesh/generated/meshtastic/rtttl.pb.cpp create mode 100644 src/mesh/generated/meshtastic/rtttl.pb.h create mode 100644 src/mesh/generated/meshtastic/storeforward.pb.cpp create mode 100644 src/mesh/generated/meshtastic/storeforward.pb.h create mode 100644 src/mesh/generated/meshtastic/telemetry.pb.cpp create mode 100644 src/mesh/generated/meshtastic/telemetry.pb.h create mode 100644 src/mesh/generated/meshtastic/xmodem.pb.cpp create mode 100644 src/mesh/generated/meshtastic/xmodem.pb.h create mode 100644 src/mesh/http/ContentHandler.cpp create mode 100644 src/mesh/http/ContentHandler.h create mode 100644 src/mesh/http/ContentHelper.cpp create mode 100644 src/mesh/http/ContentHelper.h create mode 100644 src/mesh/http/WebServer.cpp create mode 100644 src/mesh/http/WebServer.h create mode 100644 src/mesh/mesh-pb-constants.cpp create mode 100644 src/mesh/mesh-pb-constants.h create mode 100644 src/mesh/raspihttp/PiWebServer.cpp create mode 100644 src/mesh/raspihttp/PiWebServer.h create mode 100644 src/mesh/wifi/WiFiAPClient.cpp create mode 100644 src/mesh/wifi/WiFiAPClient.h create mode 100644 src/meshUtils.cpp create mode 100644 src/meshUtils.h create mode 100644 src/modules/AdminModule.cpp create mode 100644 src/modules/AdminModule.h create mode 100644 src/modules/AtakPluginModule.cpp create mode 100644 src/modules/AtakPluginModule.h create mode 100644 src/modules/CannedMessageModule.cpp create mode 100644 src/modules/CannedMessageModule.h create mode 100644 src/modules/DetectionSensorModule.cpp create mode 100644 src/modules/DetectionSensorModule.h create mode 100644 src/modules/DropzoneModule.cpp create mode 100644 src/modules/DropzoneModule.h create mode 100644 src/modules/ExternalNotificationModule.cpp create mode 100644 src/modules/ExternalNotificationModule.h create mode 100644 src/modules/ModuleDev.h create mode 100644 src/modules/Modules.cpp create mode 100644 src/modules/Modules.h create mode 100644 src/modules/NeighborInfoModule.cpp create mode 100644 src/modules/NeighborInfoModule.h create mode 100644 src/modules/NodeInfoModule.cpp create mode 100644 src/modules/NodeInfoModule.h create mode 100644 src/modules/PositionModule.cpp create mode 100644 src/modules/PositionModule.h create mode 100644 src/modules/PowerStressModule.cpp create mode 100644 src/modules/PowerStressModule.h create mode 100644 src/modules/RangeTestModule.cpp create mode 100644 src/modules/RangeTestModule.h create mode 100644 src/modules/RemoteHardwareModule.cpp create mode 100644 src/modules/RemoteHardwareModule.h create mode 100644 src/modules/ReplyModule.cpp create mode 100644 src/modules/ReplyModule.h create mode 100644 src/modules/RoutingModule.cpp create mode 100644 src/modules/RoutingModule.h create mode 100644 src/modules/SerialModule.cpp create mode 100644 src/modules/SerialModule.h create mode 100644 src/modules/StoreForwardModule.cpp create mode 100644 src/modules/StoreForwardModule.h create mode 100644 src/modules/Telemetry/AirQualityTelemetry.cpp create mode 100644 src/modules/Telemetry/AirQualityTelemetry.h create mode 100644 src/modules/Telemetry/DeviceTelemetry.cpp create mode 100644 src/modules/Telemetry/DeviceTelemetry.h create mode 100644 src/modules/Telemetry/EnvironmentTelemetry.cpp create mode 100644 src/modules/Telemetry/EnvironmentTelemetry.h create mode 100644 src/modules/Telemetry/HealthTelemetry.cpp create mode 100644 src/modules/Telemetry/HealthTelemetry.h create mode 100644 src/modules/Telemetry/PowerTelemetry.cpp create mode 100644 src/modules/Telemetry/PowerTelemetry.h create mode 100644 src/modules/Telemetry/Sensor/AHT10.cpp create mode 100644 src/modules/Telemetry/Sensor/AHT10.h create mode 100644 src/modules/Telemetry/Sensor/BME280Sensor.cpp create mode 100644 src/modules/Telemetry/Sensor/BME280Sensor.h create mode 100644 src/modules/Telemetry/Sensor/BME680Sensor.cpp create mode 100644 src/modules/Telemetry/Sensor/BME680Sensor.h create mode 100644 src/modules/Telemetry/Sensor/BMP085Sensor.cpp create mode 100644 src/modules/Telemetry/Sensor/BMP085Sensor.h create mode 100644 src/modules/Telemetry/Sensor/BMP280Sensor.cpp create mode 100644 src/modules/Telemetry/Sensor/BMP280Sensor.h create mode 100644 src/modules/Telemetry/Sensor/BMP3XXSensor.cpp create mode 100644 src/modules/Telemetry/Sensor/BMP3XXSensor.h create mode 100644 src/modules/Telemetry/Sensor/DFRobotLarkSensor.cpp create mode 100644 src/modules/Telemetry/Sensor/DFRobotLarkSensor.h create mode 100644 src/modules/Telemetry/Sensor/INA219Sensor.cpp create mode 100644 src/modules/Telemetry/Sensor/INA219Sensor.h create mode 100644 src/modules/Telemetry/Sensor/INA260Sensor.cpp create mode 100644 src/modules/Telemetry/Sensor/INA260Sensor.h create mode 100644 src/modules/Telemetry/Sensor/INA3221Sensor.cpp create mode 100644 src/modules/Telemetry/Sensor/INA3221Sensor.h create mode 100644 src/modules/Telemetry/Sensor/LPS22HBSensor.cpp create mode 100644 src/modules/Telemetry/Sensor/LPS22HBSensor.h create mode 100644 src/modules/Telemetry/Sensor/MAX17048Sensor.cpp create mode 100644 src/modules/Telemetry/Sensor/MAX17048Sensor.h create mode 100644 src/modules/Telemetry/Sensor/MAX30102Sensor.cpp create mode 100644 src/modules/Telemetry/Sensor/MAX30102Sensor.h create mode 100644 src/modules/Telemetry/Sensor/MCP9808Sensor.cpp create mode 100644 src/modules/Telemetry/Sensor/MCP9808Sensor.h create mode 100644 src/modules/Telemetry/Sensor/MLX90614Sensor.cpp create mode 100644 src/modules/Telemetry/Sensor/MLX90614Sensor.h create mode 100644 src/modules/Telemetry/Sensor/MLX90632Sensor.cpp create mode 100644 src/modules/Telemetry/Sensor/MLX90632Sensor.h create mode 100644 src/modules/Telemetry/Sensor/NAU7802Sensor.cpp create mode 100644 src/modules/Telemetry/Sensor/NAU7802Sensor.h create mode 100644 src/modules/Telemetry/Sensor/OPT3001Sensor.cpp create mode 100644 src/modules/Telemetry/Sensor/OPT3001Sensor.h create mode 100644 src/modules/Telemetry/Sensor/RCWL9620Sensor.cpp create mode 100644 src/modules/Telemetry/Sensor/RCWL9620Sensor.h create mode 100644 src/modules/Telemetry/Sensor/SHT31Sensor.cpp create mode 100644 src/modules/Telemetry/Sensor/SHT31Sensor.h create mode 100644 src/modules/Telemetry/Sensor/SHT4XSensor.cpp create mode 100644 src/modules/Telemetry/Sensor/SHT4XSensor.h create mode 100644 src/modules/Telemetry/Sensor/SHTC3Sensor.cpp create mode 100644 src/modules/Telemetry/Sensor/SHTC3Sensor.h create mode 100644 src/modules/Telemetry/Sensor/T1000xSensor.cpp create mode 100644 src/modules/Telemetry/Sensor/T1000xSensor.h create mode 100644 src/modules/Telemetry/Sensor/TSL2591Sensor.cpp create mode 100644 src/modules/Telemetry/Sensor/TSL2591Sensor.h create mode 100644 src/modules/Telemetry/Sensor/TelemetrySensor.cpp create mode 100644 src/modules/Telemetry/Sensor/TelemetrySensor.h create mode 100644 src/modules/Telemetry/Sensor/VEML7700Sensor.cpp create mode 100644 src/modules/Telemetry/Sensor/VEML7700Sensor.h create mode 100644 src/modules/Telemetry/Sensor/VoltageSensor.h create mode 100644 src/modules/Telemetry/UnitConversions.cpp create mode 100644 src/modules/Telemetry/UnitConversions.h create mode 100644 src/modules/TextMessageModule.cpp create mode 100644 src/modules/TextMessageModule.h create mode 100644 src/modules/TraceRouteModule.cpp create mode 100644 src/modules/TraceRouteModule.h create mode 100644 src/modules/WaypointModule.cpp create mode 100644 src/modules/WaypointModule.h create mode 100644 src/modules/esp32/AudioModule.cpp create mode 100644 src/modules/esp32/AudioModule.h create mode 100644 src/modules/esp32/PaxcounterModule.cpp create mode 100644 src/modules/esp32/PaxcounterModule.h create mode 100644 src/motion/AccelerometerThread.h create mode 100644 src/motion/BMA423Sensor.cpp create mode 100644 src/motion/BMA423Sensor.h create mode 100644 src/motion/BMX160Sensor.cpp create mode 100644 src/motion/BMX160Sensor.h create mode 100644 src/motion/ICM20948Sensor.cpp create mode 100644 src/motion/ICM20948Sensor.h create mode 100644 src/motion/LIS3DHSensor.cpp create mode 100644 src/motion/LIS3DHSensor.h create mode 100644 src/motion/LSM6DS3Sensor.cpp create mode 100644 src/motion/LSM6DS3Sensor.h create mode 100644 src/motion/MPU6050Sensor.cpp create mode 100644 src/motion/MPU6050Sensor.h create mode 100644 src/motion/MotionSensor.cpp create mode 100644 src/motion/MotionSensor.h create mode 100644 src/motion/QMA6100PSensor.cpp create mode 100644 src/motion/QMA6100PSensor.h create mode 100644 src/motion/STK8XXXSensor.cpp create mode 100644 src/motion/STK8XXXSensor.h create mode 100644 src/mqtt/MQTT.cpp create mode 100644 src/mqtt/MQTT.h create mode 100644 src/network-stubs.cpp create mode 100644 src/nimble/NimbleBluetooth.cpp create mode 100644 src/nimble/NimbleBluetooth.h create mode 100644 src/platform/esp32/BleOta.cpp create mode 100644 src/platform/esp32/BleOta.h create mode 100644 src/platform/esp32/ESP32CryptoEngine.cpp create mode 100644 src/platform/esp32/SimpleAllocator.cpp create mode 100644 src/platform/esp32/SimpleAllocator.h create mode 100644 src/platform/esp32/architecture.h create mode 100644 src/platform/esp32/iram-quirk.c create mode 100644 src/platform/esp32/main-esp32.cpp create mode 100644 src/platform/extra_variants/README.md create mode 100644 src/platform/extra_variants/heltec_wireless_tracker/variant.cpp create mode 100644 src/platform/nrf52/BLEDfuScure.cpp create mode 100644 src/platform/nrf52/BLEDfuSecure.h create mode 100644 src/platform/nrf52/NRF52Bluetooth.cpp create mode 100644 src/platform/nrf52/NRF52Bluetooth.h create mode 100644 src/platform/nrf52/NRF52CryptoEngine.cpp create mode 100644 src/platform/nrf52/aes-256/tiny-aes.cpp create mode 100644 src/platform/nrf52/aes-256/tiny-aes.h create mode 100644 src/platform/nrf52/alloc.cpp create mode 100644 src/platform/nrf52/architecture.h create mode 100644 src/platform/nrf52/hardfault.cpp create mode 100644 src/platform/nrf52/main-bare.cpp create mode 100644 src/platform/nrf52/main-nrf52.cpp create mode 100644 src/platform/nrf52/nrf52840_s140_v7.ld create mode 100644 src/platform/nrf52/softdevice/ble.h create mode 100644 src/platform/nrf52/softdevice/ble_err.h create mode 100644 src/platform/nrf52/softdevice/ble_gap.h create mode 100644 src/platform/nrf52/softdevice/ble_gatt.h create mode 100644 src/platform/nrf52/softdevice/ble_gattc.h create mode 100644 src/platform/nrf52/softdevice/ble_gatts.h create mode 100644 src/platform/nrf52/softdevice/ble_hci.h create mode 100644 src/platform/nrf52/softdevice/ble_l2cap.h create mode 100644 src/platform/nrf52/softdevice/ble_ranges.h create mode 100644 src/platform/nrf52/softdevice/ble_types.h create mode 100644 src/platform/nrf52/softdevice/nrf52/nrf_mbr.h create mode 100644 src/platform/nrf52/softdevice/nrf_error.h create mode 100644 src/platform/nrf52/softdevice/nrf_error_sdm.h create mode 100644 src/platform/nrf52/softdevice/nrf_error_soc.h create mode 100644 src/platform/nrf52/softdevice/nrf_nvic.h create mode 100644 src/platform/nrf52/softdevice/nrf_sdm.h create mode 100644 src/platform/nrf52/softdevice/nrf_soc.h create mode 100644 src/platform/nrf52/softdevice/nrf_svc.h create mode 100644 src/platform/portduino/PortduinoGlue.cpp create mode 100644 src/platform/portduino/PortduinoGlue.h create mode 100644 src/platform/portduino/SimRadio.cpp create mode 100644 src/platform/portduino/SimRadio.h create mode 100644 src/platform/portduino/architecture.h create mode 100644 src/platform/rp2xx0/architecture.h create mode 100644 src/platform/rp2xx0/hardware_rosc/include/hardware/rosc.h create mode 100644 src/platform/rp2xx0/hardware_rosc/rosc.c create mode 100644 src/platform/rp2xx0/main-rp2xx0.cpp create mode 100644 src/platform/rp2xx0/pico_sleep/include/pico/sleep.h create mode 100644 src/platform/rp2xx0/pico_sleep/sleep.c create mode 100644 src/platform/stm32wl/architecture.h create mode 100644 src/platform/stm32wl/main-stm32wl.cpp create mode 100644 src/power.h create mode 100644 src/serialization/JSON.cpp create mode 100644 src/serialization/JSON.h create mode 100644 src/serialization/JSONValue.cpp create mode 100644 src/serialization/JSONValue.h create mode 100644 src/serialization/MeshPacketSerializer.cpp create mode 100644 src/serialization/MeshPacketSerializer.h create mode 100644 src/serialization/MeshPacketSerializer_nRF52.cpp create mode 100644 src/shutdown.h create mode 100644 src/sleep.cpp create mode 100644 src/sleep.h create mode 100644 src/target_specific.h create mode 100644 src/xmodem.cpp create mode 100644 src/xmodem.h create mode 100644 suppressions.txt create mode 100644 test/test_crypto/test_main.cpp create mode 100644 userPrefs.h create mode 100644 userPrefs.json create mode 100644 variants/CDEBYTE_EoRa-S3/pins_arduino.h create mode 100644 variants/CDEBYTE_EoRa-S3/platformio.ini create mode 100644 variants/CDEBYTE_EoRa-S3/variant.h create mode 100644 variants/Dongle_nRF52840-pca10059-v1/platformio.ini create mode 100644 variants/Dongle_nRF52840-pca10059-v1/variant.cpp create mode 100644 variants/Dongle_nRF52840-pca10059-v1/variant.h create mode 100644 variants/EBYTE_ESP32-S3/pins_arduino.h create mode 100644 variants/EBYTE_ESP32-S3/platformio.ini create mode 100644 variants/EBYTE_ESP32-S3/variant.h create mode 100644 variants/ME25LS01-4Y10TD/platformio.ini create mode 100644 variants/ME25LS01-4Y10TD/rfswitch.h create mode 100644 variants/ME25LS01-4Y10TD/variant.cpp create mode 100644 variants/ME25LS01-4Y10TD/variant.h create mode 100644 variants/ME25LS01-4Y10TD_e-ink/platformio.ini create mode 100644 variants/ME25LS01-4Y10TD_e-ink/rfswitch.h create mode 100644 variants/ME25LS01-4Y10TD_e-ink/variant.cpp create mode 100644 variants/ME25LS01-4Y10TD_e-ink/variant.h create mode 100644 variants/MS24SF1/platformio.ini create mode 100644 variants/MS24SF1/variant.cpp create mode 100644 variants/MS24SF1/variant.h create mode 100644 variants/MakePython_nRF52840_eink/platformio.ini create mode 100644 variants/MakePython_nRF52840_eink/variant.cpp create mode 100644 variants/MakePython_nRF52840_eink/variant.h create mode 100644 variants/MakePython_nRF52840_oled/platformio.ini create mode 100644 variants/MakePython_nRF52840_oled/variant.cpp create mode 100644 variants/MakePython_nRF52840_oled/variant.h create mode 100644 variants/TWC_mesh_v4/platformio.ini create mode 100644 variants/TWC_mesh_v4/variant.cpp create mode 100644 variants/TWC_mesh_v4/variant.h create mode 100644 variants/ai-c3/platformio.ini create mode 100644 variants/ai-c3/variant.h create mode 100644 variants/betafpv_2400_tx_micro/platformio.ini create mode 100644 variants/betafpv_2400_tx_micro/variant.h create mode 100644 variants/betafpv_900_tx_nano/platformio.ini create mode 100644 variants/betafpv_900_tx_nano/variant.h create mode 100644 variants/bpi_picow_esp32_s3/pins_arduino.h create mode 100644 variants/bpi_picow_esp32_s3/platformio.ini create mode 100644 variants/bpi_picow_esp32_s3/variant.h create mode 100644 variants/canaryone/platformio.ini create mode 100644 variants/canaryone/variant.cpp create mode 100644 variants/canaryone/variant.h create mode 100644 variants/chatter2/platformio.ini create mode 100644 variants/chatter2/variant.h create mode 100644 variants/diy/dr-dev/variant.h create mode 100644 variants/diy/hydra/variant.h create mode 100644 variants/diy/mesh-tab/variant.h create mode 100644 variants/diy/nrf52_promicro_diy_tcxo/variant.cpp create mode 100644 variants/diy/nrf52_promicro_diy_tcxo/variant.h create mode 100644 variants/diy/nrf52_promicro_diy_xtal/variant.cpp create mode 100644 variants/diy/nrf52_promicro_diy_xtal/variant.h create mode 100644 variants/diy/platformio.ini create mode 100644 variants/diy/t-energy-s3_e22/variant.h create mode 100644 variants/diy/v1/variant.h create mode 100644 variants/diy/v1_1/variant.h create mode 100644 variants/dreamcatcher/platformio.ini create mode 100644 variants/dreamcatcher/rfswitch.h create mode 100644 variants/dreamcatcher/variant.h create mode 100644 variants/esp32-s3-pico/pins_arduino.h create mode 100644 variants/esp32-s3-pico/platformio.ini create mode 100644 variants/esp32-s3-pico/variant.h create mode 100644 variants/feather_diy/platformio.ini create mode 100644 variants/feather_diy/variant.cpp create mode 100644 variants/feather_diy/variant.h create mode 100644 variants/feather_rp2040_rfm95/platformio.ini create mode 100644 variants/feather_rp2040_rfm95/variant.h create mode 100644 variants/heltec_capsule_sensor_v3/platformio.ini create mode 100644 variants/heltec_capsule_sensor_v3/variant.h create mode 100644 variants/heltec_esp32c3/pins_arduino.h create mode 100644 variants/heltec_esp32c3/platformio.ini create mode 100644 variants/heltec_esp32c3/variant.h create mode 100644 variants/heltec_hru_3601/pins_arduino.h create mode 100644 variants/heltec_hru_3601/platformio.ini create mode 100644 variants/heltec_hru_3601/variant.h create mode 100644 variants/heltec_mesh_node_t114/platformio.ini create mode 100644 variants/heltec_mesh_node_t114/variant.cpp create mode 100644 variants/heltec_mesh_node_t114/variant.h create mode 100644 variants/heltec_v1/platformio.ini create mode 100644 variants/heltec_v1/variant.h create mode 100644 variants/heltec_v2.1/platformio.ini create mode 100644 variants/heltec_v2.1/variant.h create mode 100644 variants/heltec_v2/platformio.ini create mode 100644 variants/heltec_v2/variant.h create mode 100644 variants/heltec_v3/platformio.ini create mode 100644 variants/heltec_v3/variant.h create mode 100644 variants/heltec_vision_master_e213/pins_arduino.h create mode 100644 variants/heltec_vision_master_e213/platformio.ini create mode 100644 variants/heltec_vision_master_e213/variant.h create mode 100644 variants/heltec_vision_master_e290/pins_arduino.h create mode 100644 variants/heltec_vision_master_e290/platformio.ini create mode 100644 variants/heltec_vision_master_e290/variant.h create mode 100644 variants/heltec_vision_master_t190/pins_arduino.h create mode 100644 variants/heltec_vision_master_t190/platformio.ini create mode 100644 variants/heltec_vision_master_t190/variant.h create mode 100644 variants/heltec_wireless_bridge/platformio.ini create mode 100644 variants/heltec_wireless_bridge/variant.h create mode 100644 variants/heltec_wireless_paper/pins_arduino.h create mode 100644 variants/heltec_wireless_paper/platformio.ini create mode 100644 variants/heltec_wireless_paper/variant.h create mode 100644 variants/heltec_wireless_paper_v1/pins_arduino.h create mode 100644 variants/heltec_wireless_paper_v1/platformio.ini create mode 100644 variants/heltec_wireless_paper_v1/variant.h create mode 100644 variants/heltec_wireless_tracker/pins_arduino.h create mode 100644 variants/heltec_wireless_tracker/platformio.ini create mode 100644 variants/heltec_wireless_tracker/variant.h create mode 100644 variants/heltec_wireless_tracker_V1_0/pins_arduino.h create mode 100644 variants/heltec_wireless_tracker_V1_0/platformio.ini create mode 100644 variants/heltec_wireless_tracker_V1_0/variant.h create mode 100644 variants/heltec_wsl_v3/platformio.ini create mode 100644 variants/heltec_wsl_v3/variant.h create mode 100644 variants/icarus/pins_arduino.h create mode 100644 variants/icarus/platformio.ini create mode 100644 variants/icarus/variant.h create mode 100644 variants/m5stack-stamp-c3/pins_arduino.h create mode 100644 variants/m5stack-stamp-c3/platformio.ini create mode 100644 variants/m5stack-stamp-c3/variant.h create mode 100644 variants/m5stack_core/pins_arduino.h create mode 100644 variants/m5stack_core/platformio.ini create mode 100644 variants/m5stack_core/variant.h create mode 100644 variants/m5stack_coreink/pins_arduino.h create mode 100644 variants/m5stack_coreink/platformio.ini create mode 100644 variants/m5stack_coreink/variant.h create mode 100644 variants/m5stack_cores3/pins_arduino.h create mode 100644 variants/m5stack_cores3/platformio.ini create mode 100644 variants/m5stack_cores3/variant.h create mode 100644 variants/monteops_hw1/platformio.ini create mode 100644 variants/monteops_hw1/variant.cpp create mode 100644 variants/monteops_hw1/variant.h create mode 100644 variants/my_esp32s3_diy_eink/pins_arduino.h create mode 100644 variants/my_esp32s3_diy_eink/platformio.ini create mode 100644 variants/my_esp32s3_diy_eink/variant.h create mode 100644 variants/my_esp32s3_diy_oled/pins_arduino.h create mode 100644 variants/my_esp32s3_diy_oled/platformio.ini create mode 100644 variants/my_esp32s3_diy_oled/variant.h create mode 100644 variants/nano-g1-explorer/platformio.ini create mode 100644 variants/nano-g1-explorer/variant.h create mode 100644 variants/nano-g1/platformio.ini create mode 100644 variants/nano-g1/variant.h create mode 100644 variants/nano-g2-ultra/platformio.ini create mode 100644 variants/nano-g2-ultra/variant.cpp create mode 100644 variants/nano-g2-ultra/variant.h create mode 100644 variants/picomputer-s3/pins_arduino.h create mode 100644 variants/picomputer-s3/platformio.ini create mode 100644 variants/picomputer-s3/variant.h create mode 100644 variants/portduino/platformio.ini create mode 100644 variants/portduino/variant.h create mode 100644 variants/radiomaster_900_bandit/platformio.ini create mode 100644 variants/radiomaster_900_bandit/variant.h create mode 100644 variants/radiomaster_900_bandit_micro/platformio.ini create mode 100644 variants/radiomaster_900_bandit_nano/platformio.ini create mode 100644 variants/radiomaster_900_bandit_nano/variant.h create mode 100644 variants/rak10701/platformio.ini create mode 100644 variants/rak10701/variant.cpp create mode 100644 variants/rak10701/variant.h create mode 100644 variants/rak11200/pins_arduino.h create mode 100644 variants/rak11200/platformio.ini create mode 100644 variants/rak11200/variant.h create mode 100644 variants/rak11310/pins_arduino.h create mode 100644 variants/rak11310/platformio.ini create mode 100644 variants/rak11310/variant.h create mode 100644 variants/rak2560/RAK9154Sensor.cpp create mode 100644 variants/rak2560/RAK9154Sensor.h create mode 100644 variants/rak2560/create_uf2.py create mode 100644 variants/rak2560/platformio.ini create mode 100644 variants/rak2560/variant.cpp create mode 100644 variants/rak2560/variant.h create mode 100644 variants/rak3172/platformio.ini create mode 100644 variants/rak3172/variant.h create mode 100644 variants/rak4631/platformio.ini create mode 100644 variants/rak4631/variant.cpp create mode 100644 variants/rak4631/variant.h create mode 100644 variants/rak4631_epaper/platformio.ini create mode 100644 variants/rak4631_epaper/variant.cpp create mode 100644 variants/rak4631_epaper/variant.h create mode 100644 variants/rak4631_epaper_onrxtx/platformio.ini create mode 100644 variants/rak4631_epaper_onrxtx/variant.cpp create mode 100644 variants/rak4631_epaper_onrxtx/variant.h create mode 100644 variants/rak4631_eth_gw/platformio.ini create mode 100644 variants/rak4631_eth_gw/variant.cpp create mode 100644 variants/rak4631_eth_gw/variant.h create mode 100644 variants/rp2040-lora/platformio.ini create mode 100644 variants/rp2040-lora/variant.h create mode 100644 variants/rpipico-slowclock/platformio.ini create mode 100644 variants/rpipico-slowclock/variant.h create mode 100644 variants/rpipico/platformio.ini create mode 100644 variants/rpipico/variant.h create mode 100644 variants/rpipico2/platformio.ini create mode 100644 variants/rpipico2/variant.h create mode 100644 variants/rpipicow/platformio.ini create mode 100644 variants/rpipicow/variant.h create mode 100644 variants/seeed-sensecap-indicator/pins_arduino.h create mode 100644 variants/seeed-sensecap-indicator/platformio.ini create mode 100644 variants/seeed-sensecap-indicator/variant.h create mode 100644 variants/seeed_xiao_s3/pins_arduino.h create mode 100644 variants/seeed_xiao_s3/platformio.ini create mode 100644 variants/seeed_xiao_s3/variant.h create mode 100644 variants/senselora_rp2040/pins_arduino.h create mode 100644 variants/senselora_rp2040/platformio.ini create mode 100644 variants/senselora_rp2040/variant.h create mode 100644 variants/station-g1/platformio.ini create mode 100644 variants/station-g1/variant.h create mode 100644 variants/station-g2/pins_arduino.h create mode 100644 variants/station-g2/platformio.ini create mode 100644 variants/station-g2/variant.h create mode 100644 variants/t-deck/pins_arduino.h create mode 100644 variants/t-deck/platformio.ini create mode 100644 variants/t-deck/variant.h create mode 100644 variants/t-echo/platformio.ini create mode 100644 variants/t-echo/variant.cpp create mode 100644 variants/t-echo/variant.h create mode 100644 variants/t-watch-s3/pins_arduino.h create mode 100644 variants/t-watch-s3/platformio.ini create mode 100644 variants/t-watch-s3/variant.h create mode 100644 variants/tbeam-s3-core/pins_arduino.h create mode 100644 variants/tbeam-s3-core/platformio.ini create mode 100644 variants/tbeam-s3-core/variant.h create mode 100644 variants/tbeam/platformio.ini create mode 100644 variants/tbeam/variant.h create mode 100644 variants/tbeam_v07/platformio.ini create mode 100644 variants/tbeam_v07/variant.h create mode 100644 variants/tlora_c6/platformio.ini create mode 100644 variants/tlora_c6/variant.h create mode 100644 variants/tlora_t3s3_epaper/pins_arduino.h create mode 100644 variants/tlora_t3s3_epaper/platformio.ini create mode 100644 variants/tlora_t3s3_epaper/variant.h create mode 100644 variants/tlora_t3s3_v1/pins_arduino.h create mode 100644 variants/tlora_t3s3_v1/platformio.ini create mode 100644 variants/tlora_t3s3_v1/rfswitch.h create mode 100644 variants/tlora_t3s3_v1/variant.h create mode 100644 variants/tlora_v1/platformio.ini create mode 100644 variants/tlora_v1/variant.h create mode 100644 variants/tlora_v1_3/platformio.ini create mode 100644 variants/tlora_v1_3/variant.h create mode 100644 variants/tlora_v2/platformio.ini create mode 100644 variants/tlora_v2/variant.h create mode 100644 variants/tlora_v2_1_16/platformio.ini create mode 100644 variants/tlora_v2_1_16/variant.h create mode 100644 variants/tlora_v2_1_16_tcxo/platformio.ini create mode 100644 variants/tlora_v2_1_18/platformio.ini create mode 100644 variants/tlora_v2_1_18/variant.h create mode 100644 variants/tracker-t1000-e/platformio.ini create mode 100644 variants/tracker-t1000-e/rfswitch.h create mode 100644 variants/tracker-t1000-e/variant.cpp create mode 100644 variants/tracker-t1000-e/variant.h create mode 100644 variants/trackerd/platformio.ini create mode 100644 variants/trackerd/variant.h create mode 100644 variants/tracksenger/internal/pins_arduino.h create mode 100644 variants/tracksenger/internal/variant.h create mode 100644 variants/tracksenger/lcd/pins_arduino.h create mode 100644 variants/tracksenger/lcd/variant.h create mode 100644 variants/tracksenger/oled/pins_arduino.h create mode 100644 variants/tracksenger/oled/variant.h create mode 100644 variants/tracksenger/platformio.ini create mode 100644 variants/unphone/pins_arduino.h create mode 100644 variants/unphone/platformio.ini create mode 100644 variants/unphone/variant.cpp create mode 100644 variants/unphone/variant.h create mode 100644 variants/wio-e5/platformio.ini create mode 100644 variants/wio-e5/variant.h create mode 100644 variants/wio-sdk-wm1110/platformio.ini create mode 100644 variants/wio-sdk-wm1110/rfswitch.h create mode 100644 variants/wio-sdk-wm1110/variant.cpp create mode 100644 variants/wio-sdk-wm1110/variant.h create mode 100644 variants/wio-t1000-s/platformio.ini create mode 100644 variants/wio-t1000-s/rfswitch.h create mode 100644 variants/wio-t1000-s/variant.cpp create mode 100644 variants/wio-t1000-s/variant.h create mode 100644 variants/wio-tracker-wm1110/platformio.ini create mode 100644 variants/wio-tracker-wm1110/rfswitch.h create mode 100644 variants/wio-tracker-wm1110/variant.cpp create mode 100644 variants/wio-tracker-wm1110/variant.h create mode 100644 variants/wiphone/pins_arduino.h create mode 100644 variants/wiphone/platformio.ini create mode 100644 variants/wiphone/variant.h create mode 100644 variants/xiao_ble/README.md create mode 100644 variants/xiao_ble/platformio.ini create mode 100644 variants/xiao_ble/variant.cpp create mode 100644 variants/xiao_ble/variant.h create mode 100644 variants/xiao_ble/xiao-ble-internal-format.uf2 create mode 100644 variants/xiao_ble/xiao_ble.sh create mode 100644 variants/xiao_ble/xiao_nrf52840_ble_bootloader-0.7.0-22-g277a0c8_s140_7.3.0.zip create mode 100644 version.properties diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5840970 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +* text=auto eol=lf +*.{cmd,[cC][mM][dD]} text eol=crlf +*.{bat,[bB][aA][tT]} text eol=crlf +*.{sh,[sS][hH]} text eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..28f9a24 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +.pio + +# ignore vscode IDE settings files +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/extensions.json +*.code-workspace + +.idea + +.DS_Store +Thumbs.db +.autotools +.built +.context +.cproject +.vagrant +nanopb* +flash.uf2 +cmake-build* +__pycache__ + +*.swp +*.swo +*~ + +venv/ +release/ +.vscode/extensions.json +/compile_commands.json +src/mesh/raspihttp/certificate.pem +src/mesh/raspihttp/private_key.pem diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..7c54ad5 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "protobufs"] + path = protobufs + url = https://github.com/meshtastic/protobufs.git +[submodule "meshtestic"] + path = meshtestic + url = https://github.com/meshtastic/meshTestic diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 0000000..0af235a --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,2 @@ +tasks: + - init: pip install platformio && pip install --upgrade pip diff --git a/.semgrepignore b/.semgrepignore new file mode 100644 index 0000000..b4267ad --- /dev/null +++ b/.semgrepignore @@ -0,0 +1,2 @@ +.github/workflows/main_matrix.yml +src/mesh/compression/unishox2.cpp diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f579a7f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,47 @@ +# Contributing to Meshtastic Firmware + +We're excited that you're interested in contributing to the Meshtastic firmware! This document provides a high-level overview of how you can get involved. + +## Important First Steps + +Before you begin, please: + +1. **Read our documentation**: Our [official documentation](https://meshtastic.org/docs/) is a crucial resource. It contains essential information about the project. + +2. **Check out the firmware build guide**: For specific instructions on setting up your development environment and building the firmware, refer to our [Firmware Build Guide](https://meshtastic.org/docs/development/firmware/build/). + +3. Read our [Code of Conduct](https://meshtastic.org/docs/legal/conduct/) + +4. Join our [Discord community](https://discord.com/invite/ktMAKGBnBs) to connect with developers and other contributors to get help. + +## Getting Help and Discussing Ideas + +We encourage open communication and discussion before diving into code changes: + +1. **Use GitHub Discussions**: For new ideas, questions, or to discuss potential changes, start a conversation in our [GitHub Discussions](https://github.com/meshtastic/firmware/discussions) first. This helps us collaborate and avoid duplicate work. + +2. **Join our Discord**: For real-time chat and quick questions, join our [Discord server](https://discord.com/invite/ktMAKGBnBs). It's a great place to get help and connect with other developers and the community. + +3. **Reporting Issues**: If you've identified a bug, please use our bug report template when creating a new issue in the [issue tracker](https://github.com/meshtastic/firmware/issues). Ensure you've searched existing issues to avoid duplicates. + +## Making Contributions + +> [!IMPORTANT] +> Before making any contributions, you must sign our Contributor License Agreement (CLA). You can do this by visiting https://cla-assistant.io/meshtastic/firmware. Be sure to use the GitHub account you will use to submit your contributions when signing. + +1. Fork the repository +2. Create a new branch for your feature or bug fix +3. Make your changes +4. Test your changes thoroughly +5. Create a pull request with a clear description, using the provided template, of your changes. Be sure to enable "Allow edits from maintainers". + +## Coding Standards + +To ensure consistent code formatting across the project: + +1. Install the [Trunk](https://marketplace.visualstudio.com/items?itemName=Trunk.io) extension for Visual Studio Code. +2. Before submitting your changes, run `trunk fmt` to automatically format your code according to our standards. + +Adhering to these formatting guidelines helps maintain code consistency and makes the review process smoother. + +Thank you for contributing to Meshtastic! diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fc34fbd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,54 @@ +FROM debian:bookworm-slim AS builder + +ENV DEBIAN_FRONTEND=noninteractive +ENV TZ=Etc/UTC + +# http://bugs.python.org/issue19846 +# > At the moment, setting "LANG=C" on a Linux system *fundamentally breaks Python 3*, and that's not OK. +ENV LANG C.UTF-8 + +# Install build deps +USER root + +# trunk-ignore(terrascan/AC_DOCKER_0002): Known terrascan issue +# trunk-ignore(hadolint/DL3008): Use latest version of packages for buildchain +RUN apt-get update && apt-get install --no-install-recommends -y wget python3 python3-pip python3-wheel python3-venv g++ zip git \ + ca-certificates libgpiod-dev libyaml-cpp-dev libbluetooth-dev \ + libulfius-dev liborcania-dev libssl-dev pkg-config && \ + apt-get clean && rm -rf /var/lib/apt/lists/* && mkdir /tmp/firmware + +RUN groupadd -g 1000 mesh && useradd -ml -u 1000 -g 1000 mesh && chown mesh:mesh /tmp/firmware +USER mesh + +WORKDIR /tmp/firmware +RUN python3 -m venv /tmp/firmware +RUN bash -o pipefail -c "source bin/activate; pip3 install --no-cache-dir -U platformio==6.1.15" +# trunk-ignore(terrascan/AC_DOCKER_00024): We would actually like these files to be owned by mesh tyvm +COPY --chown=mesh:mesh . /tmp/firmware +RUN bash -o pipefail -c "source ./bin/activate && bash ./bin/build-native.sh" +RUN cp "/tmp/firmware/release/meshtasticd_linux_$(uname -m)" "/tmp/firmware/release/meshtasticd" + + +##### PRODUCTION BUILD ############# + +FROM debian:bookworm-slim +ENV DEBIAN_FRONTEND=noninteractive +ENV TZ=Etc/UTC + +# trunk-ignore(terrascan/AC_DOCKER_0002): Known terrascan issue +# trunk-ignore(hadolint/DL3008): Use latest version of packages for buildchain +RUN apt-get update && apt-get --no-install-recommends -y install libc-bin libc6 libgpiod2 libyaml-cpp0.7 libulfius2.7 liborcania2.3 libssl3 && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +RUN groupadd -g 1000 mesh && useradd -ml -u 1000 -g 1000 mesh +USER mesh + +WORKDIR /home/mesh +COPY --from=builder /tmp/firmware/release/meshtasticd /home/mesh/ + +RUN mkdir data +VOLUME /home/mesh/data + +CMD [ "sh", "-cx", "./meshtasticd -d /home/mesh/data --hwid=${HWID:-$RANDOM}" ] + +HEALTHCHECK NONE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..10926e8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,675 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..fb4d900 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,12 @@ +# Security Policy + +## Supported Versions + +| Firmware Version | Supported | +| ---------------- | ------------------ | +| 2.5.x | :white_check_mark: | +| <= 2.4.x | :x: | + +## Reporting a Vulnerability + +We support the private reporting of potential security vulnerabilities. Please go to the Security tab to file a report with a description of the potential vulnerability and reproduction scripts (preferred) or steps, and our developers will review. diff --git a/arch/esp32/esp32.ini b/arch/esp32/esp32.ini new file mode 100644 index 0000000..36d8b95 --- /dev/null +++ b/arch/esp32/esp32.ini @@ -0,0 +1,67 @@ +; Common settings for ESP targes, mixin with extends = esp32_base +[esp32_base] +extends = arduino_base +custom_esp32_kind = esp32 +platform = platformio/espressif32@6.9.0 + +build_src_filter = + ${arduino_base.build_src_filter} - - - - - + +upload_speed = 921600 +debug_init_break = tbreak setup +monitor_filters = esp32_exception_decoder + +board_build.filesystem = littlefs + +# Remove -DMYNEWT_VAL_BLE_HS_LOG_LVL=LOG_LEVEL_CRITICAL for low level BLE logging. +# See library directory for BLE logging possible values: .pio/libdeps/tbeam/NimBLE-Arduino/src/log_common/log_common.h +# This overrides the BLE logging default of LOG_LEVEL_INFO (1) from: .pio/libdeps/tbeam/NimBLE-Arduino/src/esp_nimble_cfg.h +build_unflags = -fno-lto +build_flags = + ${arduino_base.build_flags} + -flto + -Wall + -Wextra + -Isrc/platform/esp32 + -std=c++11 + -DLOG_LOCAL_LEVEL=ESP_LOG_DEBUG + -DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG + -DMYNEWT_VAL_BLE_HS_LOG_LVL=LOG_LEVEL_CRITICAL + -DAXP_DEBUG_PORT=Serial + -DCONFIG_BT_NIMBLE_ENABLED + -DCONFIG_NIMBLE_CPP_LOG_LEVEL=2 + -DCONFIG_BT_NIMBLE_MAX_CCCDS=20 + -DCONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=5120 + -DESP_OPENSSL_SUPPRESS_LEGACY_WARNING + -DSERIAL_BUFFER_SIZE=4096 + -DLIBPAX_ARDUINO + -DLIBPAX_WIFI + -DLIBPAX_BLE + ;-DDEBUG_HEAP + +lib_deps = + ${arduino_base.lib_deps} + ${networking_base.lib_deps} + ${environmental_base.lib_deps} + https://github.com/meshtastic/esp32_https_server.git#23665b3adc080a311dcbb586ed5941b5f94d6ea2 + h2zero/NimBLE-Arduino@^1.4.2 + https://github.com/dbSuS/libpax.git#7bcd3fcab75037505be9b122ab2b24cc5176b587 + lewisxhe/XPowersLib@^0.2.6 + https://github.com/meshtastic/ESP32_Codec2.git#633326c78ac251c059ab3a8c430fcdf25b41672f + rweather/Crypto@^0.4.0 + +lib_ignore = + segger_rtt + ESP32 BLE Arduino + +; leave this commented out to avoid breaking Windows +;upload_port = /dev/ttyUSB0 +;monitor_port = /dev/ttyUSB0 + +; Please don't delete these lines. JM uses them. +;upload_port = /dev/cu.SLAB_USBtoUART +;monitor_port = /dev/cu.SLAB_USBtoUART + +; customize the partition table +; http://docs.platformio.org/en/latest/platforms/espressif32.html#partition-tables +board_build.partitions = partition-table.csv \ No newline at end of file diff --git a/arch/esp32/esp32c3.ini b/arch/esp32/esp32c3.ini new file mode 100644 index 0000000..2ba3036 --- /dev/null +++ b/arch/esp32/esp32c3.ini @@ -0,0 +1,6 @@ +[esp32c3_base] +extends = esp32_base +custom_esp32_kind = esp32c3 + +monitor_speed = 115200 +monitor_filters = esp32_c3_exception_decoder diff --git a/arch/esp32/esp32c6.ini b/arch/esp32/esp32c6.ini new file mode 100644 index 0000000..53d7f92 --- /dev/null +++ b/arch/esp32/esp32c6.ini @@ -0,0 +1,40 @@ +[esp32c6_base] +extends = esp32_base +platform = https://github.com/Jason2866/platform-espressif32.git#22faa566df8c789000f8136cd8d0aca49617af55 +build_flags = + ${arduino_base.build_flags} + -Wall + -Wextra + -Isrc/platform/esp32 + -std=c++11 + -DESP_OPENSSL_SUPPRESS_LEGACY_WARNING + -DSERIAL_BUFFER_SIZE=4096 + -DLIBPAX_ARDUINO + -DLIBPAX_WIFI + -DLIBPAX_BLE + -DMESHTASTIC_EXCLUDE_WEBSERVER + ;-DDEBUG_HEAP + ; TEMP + -DHAS_BLUETOOTH=0 + -DMESHTASTIC_EXCLUDE_PAXCOUNTER + -DMESHTASTIC_EXCLUDE_BLUETOOTH + +lib_deps = + ${arduino_base.lib_deps} + ${networking_base.lib_deps} + ${environmental_base.lib_deps} + lewisxhe/XPowersLib@^0.2.6 + https://github.com/meshtastic/ESP32_Codec2.git#633326c78ac251c059ab3a8c430fcdf25b41672f + rweather/Crypto@^0.4.0 + +build_src_filter = + ${esp32_base.build_src_filter} - + +monitor_speed = 460800 +monitor_filters = esp32_c3_exception_decoder + +lib_ignore = + NonBlockingRTTTL + NimBLE-Arduino + libpax + \ No newline at end of file diff --git a/arch/esp32/esp32s2.ini b/arch/esp32/esp32s2.ini new file mode 100644 index 0000000..40fdc46 --- /dev/null +++ b/arch/esp32/esp32s2.ini @@ -0,0 +1,19 @@ +[esp32s2_base] +extends = esp32_base +custom_esp32_kind = esp32s2 + +build_src_filter = + ${esp32_base.build_src_filter} - - - + +monitor_speed = 115200 + +build_flags = + ${esp32_base.build_flags} + -DHAS_BLUETOOTH=0 + -DMESHTASTIC_EXCLUDE_PAXCOUNTER + -DMESHTASTIC_EXCLUDE_BLUETOOTH + +lib_ignore = + ${esp32_base.lib_ignore} + NimBLE-Arduino + libpax \ No newline at end of file diff --git a/arch/esp32/esp32s3.ini b/arch/esp32/esp32s3.ini new file mode 100644 index 0000000..1cd0e20 --- /dev/null +++ b/arch/esp32/esp32s3.ini @@ -0,0 +1,6 @@ +[esp32s3_base] +extends = esp32_base +custom_esp32_kind = esp32s3 + +monitor_speed = 115200 + diff --git a/arch/nrf52/cpp_overrides/lfs_util.h b/arch/nrf52/cpp_overrides/lfs_util.h new file mode 100644 index 0000000..bf5a347 --- /dev/null +++ b/arch/nrf52/cpp_overrides/lfs_util.h @@ -0,0 +1,208 @@ +/* + * lfs utility functions + * + * Copyright (c) 2017, Arm Limited. All rights reserved. + * 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 +// https://github.com/adafruit/Adafruit_nRF52_Arduino/blob/c25d93268a3b9c23e9a1ccfcaf9b208beca624ca/libraries/Adafruit_LittleFS/src/littlefs/lfs_util.h + +#ifndef LFS_UTIL_H +#define LFS_UTIL_H + +// Users can override lfs_util.h with their own configuration by defining +// LFS_CONFIG as a header file to include (-DLFS_CONFIG=lfs_config.h). +// +// If LFS_CONFIG is used, none of the default utils will be emitted and must be +// provided by the config file. To start I would suggest copying lfs_util.h and +// modifying as needed. +#ifdef LFS_CONFIG +#define LFS_STRINGIZE(x) LFS_STRINGIZE2(x) +#define LFS_STRINGIZE2(x) #x +#include LFS_STRINGIZE(LFS_CONFIG) +#else + +// System includes +#include +#include +#include + +#ifndef LFS_NO_MALLOC +#include +#endif +#ifndef LFS_NO_ASSERT +#include +#endif + +#if !defined(LFS_NO_DEBUG) || !defined(LFS_NO_WARN) || !defined(LFS_NO_ERROR) +#include +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +// Macros, may be replaced by system specific wrappers. Arguments to these +// macros must not have side-effects as the macros can be removed for a smaller +// code footprint + +// Logging functions +#ifndef LFS_NO_DEBUG + +void logLegacy(const char *level, const char *fmt, ...); +#define LFS_DEBUG(fmt, ...) logLegacy("DEBUG", "lfs debug:%d: " fmt "\n", __LINE__, __VA_ARGS__) +#else +#define LFS_DEBUG(fmt, ...) +#endif + +#ifndef LFS_NO_WARN +#define LFS_WARN(fmt, ...) logLegacy("WARN", "lfs warn:%d: " fmt "\n", __LINE__, __VA_ARGS__) +#else +#define LFS_WARN(fmt, ...) +#endif + +#ifndef LFS_NO_ERROR +#define LFS_ERROR(fmt, ...) logLegacy("ERROR", "lfs error:%d: " fmt "\n", __LINE__, __VA_ARGS__) +#else +#define LFS_ERROR(fmt, ...) +#endif + +// Runtime assertions +#ifndef LFS_NO_ASSERT +#define LFS_ASSERT(test) assert(test) +#else +extern void lfs_assert(const char *reason); +#define LFS_ASSERT(test) \ + if (!(test)) \ + lfs_assert(#test) +#endif + +// Builtin functions, these may be replaced by more efficient +// toolchain-specific implementations. LFS_NO_INTRINSICS falls back to a more +// 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_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) +{ +#if !defined(LFS_NO_INTRINSICS) && (defined(__GNUC__) || defined(__CC_ARM)) + 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; +#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) +{ +#if !defined(LFS_NO_INTRINSICS) && defined(__GNUC__) + return __builtin_ctz(a); +#else + 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) +{ +#if !defined(LFS_NO_INTRINSICS) && (defined(__GNUC__) || defined(__CC_ARM)) + return __builtin_popcount(a); +#else + 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); +} + +// 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) || \ + (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)) + 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); +#endif +} + +// Convert to 32-bit little-endian from native order +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) +{ +#ifndef LFS_NO_MALLOC + extern void *pvPortMalloc(size_t xWantedSize); + return pvPortMalloc(size); +#else + (void)size; + return NULL; +#endif +} + +// Deallocate memory, only used if buffers are not provided to littlefs +static inline void lfs_free(void *p) +{ +#ifndef LFS_NO_MALLOC + extern void vPortFree(void *pv); + vPortFree(p); +#else + (void)p; +#endif +} + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif +#endif \ No newline at end of file diff --git a/arch/nrf52/nrf52.ini b/arch/nrf52/nrf52.ini new file mode 100644 index 0000000..1af6819 --- /dev/null +++ b/arch/nrf52/nrf52.ini @@ -0,0 +1,27 @@ +[nrf52_base] +; Instead of the standard nordicnrf52 platform, we use our fork which has our added variant files +platform = platformio/nordicnrf52@^10.5.0 +extends = arduino_base +platform_packages = + ; our custom Git version until they merge our PR + framework-arduinoadafruitnrf52 @ https://github.com/geeksville/Adafruit_nRF52_Arduino.git + +build_type = debug +build_flags = + -include arch/nrf52/cpp_overrides/lfs_util.h + ${arduino_base.build_flags} + -DSERIAL_BUFFER_SIZE=1024 + -Wno-unused-variable + -Isrc/platform/nrf52 + -DLFS_NO_ASSERT ; Disable LFS assertions , see https://github.com/meshtastic/firmware/pull/3818 + +build_src_filter = + ${arduino_base.build_src_filter} - - - - - - - - - - + +lib_deps= + ${arduino_base.lib_deps} + rweather/Crypto@^0.4.0 + +lib_ignore = + BluetoothOTA + lvgl diff --git a/arch/nrf52/nrf52832.ini b/arch/nrf52/nrf52832.ini new file mode 100644 index 0000000..ce94283 --- /dev/null +++ b/arch/nrf52/nrf52832.ini @@ -0,0 +1,7 @@ +[nrf52832_base] +extends = nrf52_base + +build_flags = ${nrf52_base.build_flags} + +lib_deps = + ${nrf52_base.lib_deps} diff --git a/arch/nrf52/nrf52840.ini b/arch/nrf52/nrf52840.ini new file mode 100644 index 0000000..a13a600 --- /dev/null +++ b/arch/nrf52/nrf52840.ini @@ -0,0 +1,78 @@ +[nrf52840_base] +extends = nrf52_base + +build_flags = ${nrf52_base.build_flags} + +lib_deps = + ${nrf52_base.lib_deps} + ${environmental_base.lib_deps} + https://github.com/Kongduino/Adafruit_nRFCrypto.git#e31a8825ea3300b163a0a3c1ddd5de34e10e1371 + +; Common NRF52 debugging settings follow. See the Meshtastic developer docs for how to connect SWD debugging probes to your board. + +; We want the initial breakpoint at setup() instead of main(). Also we want to enable semihosting at that point so instead of +debug_init_break = tbreak setup +; we just turn off the platformio tbreak and do it in .gdbinit (where we have more flexibility for scripting) +; also we use a permanent breakpoint so it gets reused each time we restart the debugging session? +; debug_init_break = tbreak main + +; Note: add "monitor arm semihosting_redirect tcp 4444 all" if you want the stdout from the device to go to that port number instead +; (for use by meshtastic command line) +; monitor arm semihosting disable +; monitor debug_level 3 +; +; IMPORTANT: fileio must be disabled before using port 5555 - openocd ver 0.12 has a bug where if enabled it never properly parses the special :tt name +; for stdio access. +; monitor arm semihosting_redirect tcp 5555 stdio + +; Also note: it is _impossible_ to do non blocking reads on the semihost console port (an oversight when ARM specified the semihost API). +; So we'll neve be able to general purpose bi-directional communication with the device over semihosting. +debug_extra_cmds = + echo Running .gdbinit script + ;monitor arm semihosting enable + ;monitor arm semihosting_fileio enable + ;monitor arm semihosting_redirect disable + commands 1 + ; echo Breakpoint at setup() has semihosting console, connect to it with "telnet localhost 5555" + ; set wantSemihost = 1 + set useSoftDevice = 0 + end + + ; Only reprogram the board if the code has changed +debug_load_mode = modified +;debug_load_mode = manual +; We default to the stlink adapter because it is very cheap and works well, though others (such as jlink) are also supported. +;debug_tool = jlink +debug_tool = stlink +debug_speed = 4000 +;debug_tool = custom +; debug_server = +; openocd +; -f +; /usr/local/share/openocd/scripts/interface/stlink.cfg +; -f +; /usr/local/share/openocd/scripts/target/nrf52.cfg +; $PLATFORMIO_CORE_DIR/packages/tool-openocd/openocd/scripts/interface/cmsis-dap.cfg + +; Allows programming and debug via the RAK NanoDAP as the default debugger tool for the RAK4631 (it is only $10!) +; programming time is about the same as the bootloader version. +; For information on this see the meshtastic developers documentation for "Development on the NRF52" +; We manually pass in the elf file so that pyocd can reverse engineer FreeRTOS data (running threads, etc...) +;debug_server = +; pyocd +; gdbserver +; -j +; ${platformio.workspace_dir}/.. +; -t +; nrf52840 +; --semihosting +; --elf +; ${platformio.build_dir}/${this.__env__}/firmware.elf + +; If you want to debug the semihosting support you can turn on extra logging in pyocd with +; -L +; pyocd.debug.semihost.trace=debug + +; The following is not needed because it automatically tries do this +;debug_server_ready_pattern = -.*GDB server started on port \d+.* +;debug_port = localhost:3333 \ No newline at end of file diff --git a/arch/portduino/portduino.ini b/arch/portduino/portduino.ini new file mode 100644 index 0000000..7eb563d --- /dev/null +++ b/arch/portduino/portduino.ini @@ -0,0 +1,37 @@ +; The Portduino based sim environment on top of any host OS, all hardware will be simulated +[portduino_base] +platform = https://github.com/meshtastic/platform-native.git#6b3796d697481c8f6e3f4aa5c111bd9979f29e64 +framework = arduino + +build_src_filter = + ${env.build_src_filter} + - + - + - + - + - + - + - + + + - + - + - + - + - + +<../variants/portduino> + +lib_deps = + ${env.lib_deps} + ${networking_base.lib_deps} + rweather/Crypto@^0.4.0 + https://github.com/lovyan03/LovyanGFX.git#1401c28a47646fe00538d487adcb2eb3c72de805 + +build_flags = + ${arduino_base.build_flags} + -fPIC + -Isrc/platform/portduino + -DRADIOLIB_EEPROM_UNSUPPORTED + -DPORTDUINO_LINUX_HARDWARE + -lbluetooth + -lgpiod + -lyaml-cpp diff --git a/arch/rp2xx0/rp2040.ini b/arch/rp2xx0/rp2040.ini new file mode 100644 index 0000000..c004a3b --- /dev/null +++ b/arch/rp2xx0/rp2040.ini @@ -0,0 +1,25 @@ +; Common settings for rp2040 Processor based targets +[rp2040_base] +platform = https://github.com/maxgerhardt/platform-raspberrypi.git#v1.2.0-gcc12 +extends = arduino_base +platform_packages = framework-arduinopico@https://github.com/earlephilhower/arduino-pico.git#4.0.3 + +board_build.core = earlephilhower +board_build.filesystem_size = 0.5m +build_flags = + ${arduino_base.build_flags} -Wno-unused-variable -Wcast-align + -Isrc/platform/rp2xx0 + -Isrc/platform/rp2xx0/hardware_rosc/include + -Isrc/platform/rp2xx0/pico_sleep/include + -D__PLAT_RP2040__ +# -D _POSIX_THREADS +build_src_filter = + ${arduino_base.build_src_filter} - - - - - - - - - + +lib_ignore = + BluetoothOTA + +lib_deps = + ${arduino_base.lib_deps} + ${environmental_base.lib_deps} + rweather/Crypto \ No newline at end of file diff --git a/arch/rp2xx0/rp2350.ini b/arch/rp2xx0/rp2350.ini new file mode 100644 index 0000000..7ef6332 --- /dev/null +++ b/arch/rp2xx0/rp2350.ini @@ -0,0 +1,24 @@ +; Common settings for rp2040 Processor based targets +[rp2350_base] +platform = https://github.com/maxgerhardt/platform-raspberrypi.git#9e55f6db5c56b9867c69fe473f388beea4546672 +extends = arduino_base +platform_packages = framework-arduinopico@https://github.com/earlephilhower/arduino-pico.git#a6ab6e1f95bc1428d667d55ea7173c0744acc03c ; 4.0.2+ + +board_build.core = earlephilhower +board_build.filesystem_size = 0.5m +build_flags = + ${arduino_base.build_flags} -Wno-unused-variable + -Isrc/platform/rp2xx0 + -D__PLAT_RP2040__ +# -D _POSIX_THREADS +build_src_filter = + ${arduino_base.build_src_filter} - - - - - - - - - + +lib_ignore = + BluetoothOTA + lvgl + +lib_deps = + ${arduino_base.lib_deps} + ${environmental_base.lib_deps} + rweather/Crypto diff --git a/arch/stm32/stm32.ini b/arch/stm32/stm32.ini new file mode 100644 index 0000000..715e8aa --- /dev/null +++ b/arch/stm32/stm32.ini @@ -0,0 +1,37 @@ +[stm32_base] +extends = arduino_base +platform = ststm32 +platform_packages = platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32.git#ea74156acd823b6d14739f389e6cdc648f8ee36e + +build_type = release + +;board_build.flash_offset = 0x08000000 + +build_flags = + ${arduino_base.build_flags} + -flto + -Isrc/platform/stm32wl -g + -DMESHTASTIC_MINIMIZE_BUILD + -DMESHTASTIC_EXCLUDE_GPS + -DDEBUG_MUTE +; -DVECT_TAB_OFFSET=0x08000000 + -DconfigUSE_CMSIS_RTOS_V2=1 +; -DSPI_MODE_0=SPI_MODE0 + -fmerge-all-constants + -ffunction-sections + -fdata-sections + +build_src_filter = + ${arduino_base.build_src_filter} - - - - - - - - - - - - - - + +board_upload.offset_address = 0x08000000 +upload_protocol = stlink + +lib_deps = + ${env.lib_deps} + charlesbaynham/OSFS@^1.2.3 + https://github.com/caveman99/Crypto.git#f61ae26a53f7a2d0ba5511625b8bf8eff3a35d5e + +lib_ignore = + mathertel/OneButton@~2.6.1 + Wire \ No newline at end of file diff --git a/bin/Meshtastic_nRF52_factory_erase_v3_S140_6.1.0.uf2 b/bin/Meshtastic_nRF52_factory_erase_v3_S140_6.1.0.uf2 new file mode 100644 index 0000000000000000000000000000000000000000..e44d9da2a7934c1230927695ce899dc928faa452 GIT binary patch literal 126976 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 literal 0 HcmV?d00001 diff --git a/bin/Meshtastic_nRF52_factory_erase_v3_S140_7.3.0.uf2 b/bin/Meshtastic_nRF52_factory_erase_v3_S140_7.3.0.uf2 new file mode 100644 index 0000000000000000000000000000000000000000..73537a7358bf4e217e1218ceeb4ce7d317cdfc13 GIT binary patch literal 122368 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 v_xzx8xj)eEf~HJYRP+zn@z1U!k9wBmfPRg(;&2 + exit 1 + ;; + esac +done +shift "$((OPTIND - 1))" + +[ -z "$FILENAME" -a -n "$1" ] && { + FILENAME=$1 + shift +} + +if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then + echo "Trying to flash ${FILENAME}, but first erasing and writing system information" + "$PYTHON" -m esptool erase_flash + "$PYTHON" -m esptool write_flash 0x00 ${FILENAME} + # Account for S3 board's different OTA partition + if [ -n "${FILENAME##*"s3"*}" ] && [ -n "${FILENAME##*"-v3"*}" ] && [ -n "${FILENAME##*"t-deck"*}" ] && [ -n "${FILENAME##*"wireless-paper"*}" ] && [ -n "${FILENAME##*"wireless-tracker"*}" ] && [ -n "${FILENAME##*"station-g2"*}" ] && [ -n "${FILENAME##*"unphone"*}" ]; then + if [ -n "${FILENAME##*"esp32c3"*}" ]; then + "$PYTHON" -m esptool write_flash 0x260000 bleota.bin + else + "$PYTHON" -m esptool write_flash 0x260000 bleota-c3.bin + fi + else + "$PYTHON" -m esptool write_flash 0x260000 bleota-s3.bin + fi + "$PYTHON" -m esptool write_flash 0x300000 littlefs-*.bin + +else + show_help + echo "Invalid file: ${FILENAME}" +fi + +exit 0 diff --git a/bin/device-update.bat b/bin/device-update.bat new file mode 100644 index 0000000..2ac649b --- /dev/null +++ b/bin/device-update.bat @@ -0,0 +1,40 @@ +@ECHO OFF + +set PYTHON=python + +goto GETOPTS +:HELP +echo Usage: %~nx0 [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME^|FILENAME] +echo Flash image file to device, leave existing system intact. +echo. +echo -h Display this help and exit +echo -p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerrous). +echo -P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: %PYTHON%) +echo -f FILENAME The *update.bin file to flash. Custom to your device type. +goto EOF + +:GETOPTS +if /I "%1"=="-h" goto HELP +if /I "%1"=="--help" goto HELP +if /I "%1"=="-F" set "FILENAME=%2" & SHIFT +if /I "%1"=="-p" set ESPTOOL_PORT=%2 & SHIFT +if /I "%1"=="-P" set PYTHON=%2 & SHIFT +SHIFT +IF NOT "__%1__"=="____" goto GETOPTS + +IF "__%FILENAME%__" == "____" ( + echo "Missing FILENAME" + goto HELP +) +IF EXIST %FILENAME% IF NOT x%FILENAME:update=%==x%FILENAME% ( + echo Trying to flash update %FILENAME% + %PYTHON% -m esptool --baud 115200 write_flash 0x10000 %FILENAME% +) else ( + echo "Invalid file: %FILENAME%" + goto HELP +) else ( + echo "Invalid file: %FILENAME%" + goto HELP +) + +:EOF diff --git a/bin/device-update.sh b/bin/device-update.sh new file mode 100644 index 0000000..7233f61 --- /dev/null +++ b/bin/device-update.sh @@ -0,0 +1,54 @@ +#!/bin/sh + +PYTHON=${PYTHON:-$(which python3 python|head -n 1)} + +# Usage info +show_help() { +cat << EOF +Usage: $(basename $0) [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME|FILENAME] +Flash image file to device, leave existing system intact." + + -h Display this help and exit + -p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerrous). + -P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON") + -f FILENAME The *update.bin file to flash. Custom to your device type. + +EOF +} + + +while getopts ":hp:P:f:" opt; do + case "${opt}" in + h) + show_help + exit 0 + ;; + p) export ESPTOOL_PORT=${OPTARG} + ;; + P) PYTHON=${OPTARG} + ;; + f) FILENAME=${OPTARG} + ;; + *) + echo "Invalid flag." + show_help >&2 + exit 1 + ;; + esac +done +shift "$((OPTIND-1))" + +[ -z "$FILENAME" -a -n "$1" ] && { + FILENAME=$1 + shift +} + +if [ -f "${FILENAME}" ] && [ -z "${FILENAME##*"update"*}" ]; then + printf "Trying to flash update ${FILENAME}" + $PYTHON -m esptool --baud 115200 write_flash 0x10000 ${FILENAME} +else + show_help + echo "Invalid file: ${FILENAME}" +fi + +exit 0 diff --git a/bin/dump-ram-users.sh b/bin/dump-ram-users.sh new file mode 100644 index 0000000..3cc3ce5 --- /dev/null +++ b/bin/dump-ram-users.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +arm-none-eabi-readelf -s -e .pio/build/nrf52dk/firmware.elf | head -80 + +nm -CSr --size-sort .pio/build/nrf52dk/firmware.elf | grep '^200' diff --git a/bin/exception_decoder.py b/bin/exception_decoder.py new file mode 100644 index 0000000..ec94ce2 --- /dev/null +++ b/bin/exception_decoder.py @@ -0,0 +1,390 @@ +#!/usr/bin/env python3 + +"""ESP Exception Decoder + +github: https://github.com/janLo/EspArduinoExceptionDecoder +license: GPL v3 +author: Jan Losinski + +Meshtastic notes: +* original version is at: https://github.com/janLo/EspArduinoExceptionDecoder +* version that's checked into meshtastic repo is based on: https://github.com/me21/EspArduinoExceptionDecoder + which adds in ESP32 Backtrace decoding. +* this also updates the defaults to use ESP32, instead of ESP8266 and defaults to the built firmware.bin +* also updated the toolchain name, which will be set according to the platform + +To use, copy the "Backtrace: 0x...." line to a file, e.g., backtrace.txt, then run: +$ bin/exception_decoder.py backtrace.txt +For a platform other than ESP32, use the -p option, e.g.: +$ bin/exception_decoder.py -p ESP32S3 backtrace.txt +To specify a specific .elf file, use the -e option, e.g.: +$ bin/exception_decoder.py -e firmware.elf backtrace.txt +""" + +import argparse +import os +import re +import subprocess +import sys +from collections import namedtuple + +EXCEPTIONS = [ + "Illegal instruction", + "SYSCALL instruction", + "InstructionFetchError: Processor internal physical address or data error during instruction fetch", + "LoadStoreError: Processor internal physical address or data error during load or store", + "Level1Interrupt: Level-1 interrupt as indicated by set level-1 bits in the INTERRUPT register", + "Alloca: MOVSP instruction, if caller's registers are not in the register file", + "IntegerDivideByZero: QUOS, QUOU, REMS, or REMU divisor operand is zero", + "reserved", + "Privileged: Attempt to execute a privileged operation when CRING ? 0", + "LoadStoreAlignmentCause: Load or store to an unaligned address", + "reserved", + "reserved", + "InstrPIFDataError: PIF data error during instruction fetch", + "LoadStorePIFDataError: Synchronous PIF data error during LoadStore access", + "InstrPIFAddrError: PIF address error during instruction fetch", + "LoadStorePIFAddrError: Synchronous PIF address error during LoadStore access", + "InstTLBMiss: Error during Instruction TLB refill", + "InstTLBMultiHit: Multiple instruction TLB entries matched", + "InstFetchPrivilege: An instruction fetch referenced a virtual address at a ring level less than CRING", + "reserved", + "InstFetchProhibited: An instruction fetch referenced a page mapped with an attribute that does not permit instruction fetch", + "reserved", + "reserved", + "reserved", + "LoadStoreTLBMiss: Error during TLB refill for a load or store", + "LoadStoreTLBMultiHit: Multiple TLB entries matched for a load or store", + "LoadStorePrivilege: A load or store referenced a virtual address at a ring level less than CRING", + "reserved", + "LoadProhibited: A load referenced a page mapped with an attribute that does not permit loads", + "StoreProhibited: A store referenced a page mapped with an attribute that does not permit stores", +] + +PLATFORMS = { + "ESP8266": "xtensa-lx106", + "ESP32": "xtensa-esp32", + "ESP32S3": "xtensa-esp32s3", + "ESP32C3": "riscv32-esp", +} +TOOLS = { + "ESP8266": "xtensa", + "ESP32": "xtensa-esp32", + "ESP32S3": "xtensa-esp32s3", + "ESP32C3": "riscv32-esp", +} + +BACKTRACE_REGEX = re.compile( + r"(?:\s+(0x40[0-2](?:\d|[a-f]|[A-F]){5}):0x(?:\d|[a-f]|[A-F]){8})\b" +) +EXCEPTION_REGEX = re.compile("^Exception \\((?P[0-9]*)\\):$") +COUNTER_REGEX = re.compile( + "^epc1=(?P0x[0-9a-f]+) epc2=(?P0x[0-9a-f]+) epc3=(?P0x[0-9a-f]+) " + "excvaddr=(?P0x[0-9a-f]+) depc=(?P0x[0-9a-f]+)$" +) +CTX_REGEX = re.compile("^ctx: (?P.+)$") +POINTER_REGEX = re.compile( + "^sp: (?P[0-9a-f]+) end: (?P[0-9a-f]+) offset: (?P[0-9a-f]+)$" +) +STACK_BEGIN = ">>>stack>>>" +STACK_END = "<<[0-9a-f]+):\W+(?P[0-9a-f]+) (?P[0-9a-f]+) (?P[0-9a-f]+) (?P[0-9a-f]+)(\W.*)?$" +) + +StackLine = namedtuple("StackLine", ["offset", "content"]) + + +class ExceptionDataParser(object): + def __init__(self): + self.exception = None + + self.epc1 = None + self.epc2 = None + self.epc3 = None + self.excvaddr = None + self.depc = None + + self.ctx = None + + self.sp = None + self.end = None + self.offset = None + + self.stack = [] + + def _parse_backtrace(self, line): + if line.startswith("Backtrace:"): + self.stack = [ + StackLine(offset=0, content=(addr,)) + for addr in BACKTRACE_REGEX.findall(line) + ] + return None + return self._parse_backtrace + + def _parse_exception(self, line): + match = EXCEPTION_REGEX.match(line) + if match is not None: + self.exception = int(match.group("exc")) + return self._parse_counters + return self._parse_exception + + def _parse_counters(self, line): + match = COUNTER_REGEX.match(line) + if match is not None: + self.epc1 = match.group("epc1") + self.epc2 = match.group("epc2") + self.epc3 = match.group("epc3") + self.excvaddr = match.group("excvaddr") + self.depc = match.group("depc") + return self._parse_ctx + return self._parse_counters + + def _parse_ctx(self, line): + match = CTX_REGEX.match(line) + if match is not None: + self.ctx = match.group("ctx") + return self._parse_pointers + return self._parse_ctx + + def _parse_pointers(self, line): + match = POINTER_REGEX.match(line) + if match is not None: + self.sp = match.group("sp") + self.end = match.group("end") + self.offset = match.group("offset") + return self._parse_stack_begin + return self._parse_pointers + + def _parse_stack_begin(self, line): + if line == STACK_BEGIN: + return self._parse_stack_line + return self._parse_stack_begin + + def _parse_stack_line(self, line): + if line != STACK_END: + match = STACK_REGEX.match(line) + if match is not None: + self.stack.append( + StackLine( + offset=match.group("off"), + content=( + match.group("c1"), + match.group("c2"), + match.group("c3"), + match.group("c4"), + ), + ) + ) + return self._parse_stack_line + return None + + def parse_file(self, file, platform, stack_only=False): + if platform != "ESP8266": + func = self._parse_backtrace + else: + func = self._parse_exception + if stack_only: + func = self._parse_stack_begin + + for line in file: + func = func(line.strip()) + if func is None: + break + + if func is not None: + print("ERROR: Parser not complete!") + sys.exit(1) + + +class AddressResolver(object): + def __init__(self, tool_path, elf_path): + self._tool = tool_path + self._elf = elf_path + self._address_map = {} + + def _lookup(self, addresses): + cmd = [self._tool, "-aipfC", "-e", self._elf] + [ + addr for addr in addresses if addr is not None + ] + + if sys.version_info[0] < 3: + output = subprocess.check_output(cmd) + else: + output = subprocess.check_output(cmd, encoding="utf-8") + + line_regex = re.compile("^(?P[0-9a-fx]+): (?P.+)$") + + last = None + for line in output.splitlines(): + line = line.strip() + match = line_regex.match(line) + + if match is None: + if last is not None and line.startswith("(inlined by)"): + line = line[12:].strip() + self._address_map[last] += "\n \-> inlined by: " + line + continue + + if match.group("result") == "?? ??:0": + continue + + self._address_map[match.group("addr")] = match.group("result") + last = match.group("addr") + + def fill(self, parser): + addresses = [ + parser.epc1, + parser.epc2, + parser.epc3, + parser.excvaddr, + parser.sp, + parser.end, + parser.offset, + ] + for line in parser.stack: + addresses.extend(line.content) + + self._lookup(addresses) + + def _sanitize_addr(self, addr): + if addr.startswith("0x"): + addr = addr[2:] + + fill = "0" * (8 - len(addr)) + return "0x" + fill + addr + + def resolve_addr(self, addr): + out = self._sanitize_addr(addr) + + if out in self._address_map: + out += ": " + self._address_map[out] + + return out + + def resolve_stack_addr(self, addr, full=True): + addr = self._sanitize_addr(addr) + if addr in self._address_map: + return addr + ": " + self._address_map[addr] + + if full: + return "[DATA (0x" + addr + ")]" + + return None + + +def print_addr(name, value, resolver): + print("{}:{} {}".format(name, " " * (8 - len(name)), resolver.resolve_addr(value))) + + +def print_stack_full(lines, resolver): + print("stack:") + for line in lines: + print(str(line.offset) + ":") + for content in line.content: + print(" " + resolver.resolve_stack_addr(content)) + + +def print_stack(lines, resolver): + print("stack:") + for line in lines: + for content in line.content: + out = resolver.resolve_stack_addr(content, full=False) + if out is None: + continue + print(out) + + +def print_result(parser, resolver, platform, full=True, stack_only=False): + if platform == "ESP8266" and not stack_only: + print( + "Exception: {} ({})".format(parser.exception, EXCEPTIONS[parser.exception]) + ) + + print("") + print_addr("epc1", parser.epc1, resolver) + print_addr("epc2", parser.epc2, resolver) + print_addr("epc3", parser.epc3, resolver) + print_addr("excvaddr", parser.excvaddr, resolver) + print_addr("depc", parser.depc, resolver) + + print("") + print("ctx: " + parser.ctx) + + print("") + print_addr("sp", parser.sp, resolver) + print_addr("end", parser.end, resolver) + print_addr("offset", parser.offset, resolver) + + print("") + if full: + print_stack_full(parser.stack, resolver) + else: + print_stack(parser.stack, resolver) + + +def parse_args(): + parser = argparse.ArgumentParser(description="decode ESP Stacktraces.") + + parser.add_argument( + "-p", + "--platform", + help="The platform to decode from", + choices=PLATFORMS.keys(), + default="ESP32", + ) + parser.add_argument( + "-t", + "--tool", + help="Path to the toolchain (without specific platform)", + default="~/.platformio/packages/toolchain-", + ) + parser.add_argument( + "-e", "--elf", help="path to elf file", default=".pio/build/tbeam/firmware.elf" + ) + parser.add_argument( + "-f", "--full", help="Print full stack dump", action="store_true" + ) + parser.add_argument( + "-s", "--stack_only", help="Decode only a stractrace", action="store_true" + ) + parser.add_argument( + "file", + help="The file to read the exception data from ('-' for STDIN)", + default="-", + ) + + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + + if args.file == "-": + file = sys.stdin + else: + if not os.path.exists(args.file): + print("ERROR: file " + args.file + " not found") + sys.exit(1) + file = open(args.file, "r") + + addr2line = os.path.join( + os.path.abspath(os.path.expanduser(args.tool + TOOLS[args.platform])), + "bin/" + PLATFORMS[args.platform] + "-elf-addr2line", + ) + if os.name == "nt": + addr2line += ".exe" + if not os.path.exists(addr2line): + print("ERROR: addr2line not found (" + addr2line + ")") + + elf_file = os.path.abspath(os.path.expanduser(args.elf)) + if not os.path.exists(elf_file): + print("ERROR: elf file not found (" + elf_file + ")") + + parser = ExceptionDataParser() + resolver = AddressResolver(addr2line, elf_file) + + parser.parse_file(file, args.platform, args.stack_only) + resolver.fill(parser) + + print_result(parser, resolver, args.platform, args.full, args.stack_only) diff --git a/bin/gen-images.sh b/bin/gen-images.sh new file mode 100644 index 0000000..0c10fdd --- /dev/null +++ b/bin/gen-images.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -e + +# regen the design bins first +cd design +bin/generate-pngs.sh +cd .. + +# assumes 50 wide, 28 high +convert design/logo/png/Mesh_Logo_Black_Small.png -background white -alpha Background src/graphics/img/icon.xbm + +inkscape --batch-process -o images/compass.png -w 48 -h 48 images/location_searching-24px.svg +convert compass.png -background white -alpha Background src/graphics/img/compass.xbm + +inkscape --batch-process -o images/face.png -w 13 -h 13 images/face-24px.svg + +inkscape --batch-process -o images/pin.png -w 13 -h 13 images/room-24px.svg +convert pin.png -background white -alpha Background src/graphics/img/pin.xbm diff --git a/bin/generate_ci_matrix.py b/bin/generate_ci_matrix.py new file mode 100644 index 0000000..4d8759e --- /dev/null +++ b/bin/generate_ci_matrix.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python + +"""Generate the CI matrix.""" + +import configparser +import json +import os +import sys +import random + +rootdir = "variants/" + +options = sys.argv[1:] + +outlist = [] + +if len(options) < 1: + print(json.dumps(outlist)) + exit() + +for subdir, dirs, files in os.walk(rootdir): + for file in files: + if file == "platformio.ini": + config = configparser.ConfigParser() + config.read(subdir + "/" + file) + for c in config.sections(): + if c.startswith("env:"): + section = config[c].name[4:] + if "extends" in config[config[c].name]: + if config[config[c].name]["extends"] == options[0] + "_base": + if "board_level" in config[config[c].name]: + if ( + config[config[c].name]["board_level"] == "extra" + ) & ("extra" in options): + outlist.append(section) + else: + outlist.append(section) + if "board_check" in config[config[c].name]: + if (config[config[c].name]["board_check"] == "true") & ( + "check" in options + ): + outlist.append(section) +if ("quick" in options) & (len(outlist) > 3): + print(json.dumps(random.sample(outlist, 3))) +else: + print(json.dumps(outlist)) diff --git a/bin/generic/Meshtastic_6.1.0_bootloader-0.9.2_s140_6.1.1.hex b/bin/generic/Meshtastic_6.1.0_bootloader-0.9.2_s140_6.1.1.hex new file mode 100644 index 0000000..eebabf4 --- /dev/null +++ b/bin/generic/Meshtastic_6.1.0_bootloader-0.9.2_s140_6.1.1.hex @@ -0,0 +1,11744 @@ +:04000003F000B2CD8A +:020000040000FA +:1000000000040020810A000015070000610A0000BA +:100010001F07000029070000330700000000000050 +:10002000000000000000000000000000A50A000021 +:100030003D070000000000004707000051070000D6 +:100040005B070000650700006F07000079070000EC +:10005000830700008D07000097070000A10700003C +:10006000AB070000B5070000BF070000C90700008C +:10007000D3070000DD070000E7070000F1070000DC +:10008000FB070000050800000F0800001908000029 +:10009000230800002D080000370800004108000078 +:1000A0004B080000550800005F08000069080000C8 +:1000B000730800007D080000870800009108000018 +:1000C0009B080000A5080000AF080000B908000068 +:1000D000C3080000CD080000D7080000E1080000B8 +:1000E000EB080000F5080000FF0800000909000007 +:1000F000130900001D090000270900003109000054 +:100100003B0900001FB500F003F88DE80F001FBD8C +:1001100000F0ACBC40F6FC7108684FF01022401CA7 +:1001200008D00868401C09D00868401C04D0086842 +:1001300000F037BA9069F5E79069F9E7704770B554 +:100140000B46010B184400F6FF70040B4FF0805073 +:100150000022090303692403406943431D1B104621 +:1001600000F048FA29462046BDE8704000F042BA47 +:10017000F0B54FF6FF734FF4B4751A466E1E11E0DA +:10018000A94201D3344600E00C46091B30F8027B3B +:10019000641E3B441A44F9D19CB204EB134394B25D +:1001A00004EB12420029EBD198B200EB134002EBB2 +:1001B000124140EA0140F0BDF34992B00446D1E952 +:1001C0000001CDE91001FF224021684600F0F4FB58 +:1001D00094E80F008DE80F00684610A902E004C8FB +:1001E00041F8042D8842FAD110216846FFF7C0FF7C +:1001F0001090AA208DF8440000F099F9FFF78AFFCB +:1002000040F6FC7420684FF01025401C0FD0206889 +:1002100010226946803000F078F92068401C08D030 +:100220002068082210A900F070F900F061F9A869AF +:10023000EEE7A869F5E74FF080500369406940F6A2 +:10024000FC71434308684FF01022401C06D0086838 +:1002500000F58050834203D2092070479069F7E788 +:100260000868401C04D00868401C03D00020704778 +:100270009069F9E70420704770B504460068C34DE3 +:10028000072876D2DFE800F033041929631E250021 +:10029000D4E9026564682946304600F062F92A46CE +:1002A0002146304600F031F9AA002146304600F0E0 +:1002B00057FB002800D0032070BD00F009FC4FF46C +:1002C000805007E0201D00F040F90028F4D100F034 +:1002D000FFFB60682860002070BD241D94E80700C3 +:1002E000920000F03DFB0028F6D00E2070BDFFF715 +:1002F000A2FF0028FAD1D4E901034FF0805100EBAE +:10030000830208694D69684382420ED840F6F8704E +:1003100005684FF010226D1C09D0056805EB8305B8 +:100320000B6949694B439D4203D9092070BD55694A +:10033000F4E70168491C03D00068401C02D003E0C8 +:100340005069FAE70F2070BD2046FFF735FFFFF731 +:1003500072FF0028F7D1201D00F0F7F80028F2D135 +:1003600060680028F0D100F0E2F8FFF7D3FE00F05B +:10037000BFF8072070BD10B50C46182802D0012028 +:10038000086010BD2068FFF777FF206010BD41684E +:10039000054609B1012700E0002740F6F8742068FF +:1003A0004FF01026401C2BD02068AA68920000F065 +:1003B000D7FA38B3A86881002068401C27D020688D +:1003C000FFF7BDFED7B12068401C22D026684FF051 +:1003D0008050AC686D68016942695143A9420DD9EA +:1003E000016940694143A14208D92146304600F0E5 +:1003F000B8F822462946304600F087F800F078F831 +:100400007069D2E700F093F8FFF784FEF6E77069B1 +:10041000D6E77669DBE740F6FC7420684FF01026DB +:10042000401C23D02068401C0CD02068401C1FD0EA +:100430002568206805F18005401C1BD027683879A5 +:10044000AA2819D040F6F8700168491C42D001680A +:10045000491C45D00168491C3ED001680968491C07 +:100460003ED00168491C39D000683EE0B069DAE747 +:10047000B569DEE7B769E2E710212846FFF778FEA5 +:100480003968814222D12068401C05D0D4F8001080 +:1004900001F18002C03107E0B169F9E730B108CA63 +:1004A00051F8040D984201D1012000E000208A4259 +:1004B000F4D158B1286810B1042803D0FEE72846CB +:1004C000FFF765FF3149686808600EE0FFF722FE1C +:1004D00000F00EF87169BBE77169BFE7706904E06D +:1004E0004FF480500168491C01D000F0CBFAFEE7C0 +:1004F000BFF34F8F26480168264A01F4E06111439B +:100500000160BFF34F8F00BFFDE72DE9F0411746B3 +:100510000D460646002406E03046296800F054F8EF +:10052000641C2D1D361DBC42F6D3BDE8F08140F69B +:10053000FC700168491C04D0D0F800004FF48051D1 +:10054000FDE54FF010208069F8E74FF080510A690F +:10055000496900684A43824201D810207047002050 +:10056000704770B50C4605464FF4806608E0284693 +:1005700000F017F8B44205D3A4F5806405F5805562 +:10058000002CF4D170BD0000F40A0000000000202F +:100590000CED00E00400FA05144801680029FCD0C5 +:1005A0007047134A0221116010490B68002BFCD0E0 +:1005B0000F4B1B1D186008680028FCD0002010603D +:1005C00008680028FCD07047094B10B501221A605A +:1005D000064A1468002CFCD0016010680028FCD08A +:1005E0000020186010680028FCD010BD00E4014015 +:1005F00004E5014070B50C46054600F073F810B9EB +:1006000000F07EF828B121462846BDE8704000F091 +:1006100007B821462846BDE8704000F037B8000012 +:100620007FB5002200920192029203920A0B000B06 +:100630006946012302440AE0440900F01F0651F80C +:10064000245003FA06F6354341F82450401C8242F8 +:10065000F2D80D490868009A10430860081D016827 +:10066000019A1143016000F03DF800280AD00649C4 +:1006700010310868029A10430860091D0868039A3F +:10068000104308607FBD00000006004030B50F4CED +:10069000002200BF04EB0213D3F800582DB9D3F8A1 +:1006A000045815B9D3F808581DB1521C082AF1D3C3 +:1006B00030BD082AFCD204EB0212C2F80008C3F8CD +:1006C00004180220C3F8080830BD000000E0014013 +:1006D0004FF08050D0F83001082801D0002070473A +:1006E000012070474FF08050D0F83011062905D016 +:1006F000D0F83001401C01D0002070470120704725 +:100700004FF08050D0F830010A2801D00020704707 +:100710000120704708208F490968095808471020B0 +:100720008C4909680958084714208A4909680958FA +:100730000847182087490968095808473020854923 +:100740000968095808473820824909680958084744 +:100750003C20804909680958084740207D490968BC +:100760000958084744207B49096809580847482028 +:1007700078490968095808474C207649096809589A +:10078000084750207349096809580847542071499F +:1007900009680958084758206E49096809580847E8 +:1007A0005C206C4909680958084760206949096854 +:1007B00009580847642067490968095808476820AC +:1007C00064490968095808476C2062490968095852 +:1007D000084770205F4909680958084774205D4937 +:1007E00009680958084778205A490968095808478C +:1007F0007C205849096809580847802055490968EC +:10080000095808478420534909680958084788202F +:1008100050490968095808478C204E490968095809 +:10082000084790204B4909680958084794204949CE +:10083000096809580847982046490968095808472F +:100840009C204449096809580847A0204149096883 +:1008500009580847A4203F49096809580847A820B3 +:100860003C49096809580847AC203A4909680958C1 +:100870000847B0203749096809580847B420354966 +:10088000096809580847B8203249096809580847D3 +:10089000BC203049096809580847C0202D4909681B +:1008A00009580847C4202B49096809580847C82037 +:1008B0002849096809580847CC2026490968095879 +:1008C0000847D0202349096809580847D4202149FE +:1008D000096809580847D8201E4909680958084777 +:1008E000DC201C49096809580847E02019490968B3 +:1008F00009580847E4201749096809580847E820BB +:100900001449096809580847EC2012490968095830 +:100910000847F0200F49096809580847F4200D4995 +:10092000096809580847F8200A490968095808471A +:10093000FC2008490968095808475FF48070054998 +:10094000096809580847000003480449024A034B54 +:100950007047000000000020000B0000000B0000AA +:1009600040EA010310B59B070FD1042A0DD310C82C +:1009700008C9121F9C42F8D020BA19BA884201D97E +:10098000012010BD4FF0FF3010BD1AB1D30703D0C6 +:10099000521C07E0002010BD10F8013B11F8014B7C +:1009A0001B1B07D110F8013B11F8014B1B1B01D198 +:1009B000921EF1D1184610BD02F0FF0343EA032254 +:1009C00042EA024200F005B87047704770474FF0A6 +:1009D00000020429C0F0128010F0030C00F01B800C +:1009E000CCF1040CBCF1020F18BF00F8012BA8BF1A +:1009F00020F8022BA1EB0C0100F00DB85FEAC17CDE +:100A000024BF00F8012B00F8012B48BF00F8012B90 +:100A100070474FF0000200B51346944696462039C1 +:100A200022BFA0E80C50A0E80C50B1F12001BFF4A7 +:100A3000F7AF090728BFA0E80C5048BF0CC05DF80D +:100A400004EB890028BF40F8042B08BF704748BF5B +:100A500020F8022B11F0804F18BF00F8012B7047CF +:100A6000014B1B68DB6818470000002009480A4951 +:100A70007047FFF7FBFFFFF745FB00BD20BFFDE719 +:100A8000064B1847064A1060016881F308884068E1 +:100A900000470000000B0000000B000017040000DE +:100AA000000000201EF0040F0CBFEFF30881EFF3ED +:100AB0000981886902380078182803D100E0000015 +:100AC000074A1047074A12682C3212681047000084 +:100AD00000B5054B1B68054A9B58984700BD0000B0 +:100AE0007703000000000020F00A0000040000006E +:100AF000001000000000000000FFFFFF0090D00386 +:1010000080130020B157020069C00000175702008A +:1010100069C0000069C0000069C000000000000055 +:101020000000000000000000000000000D58020059 +:1010300069C000000000000069C0000069C0000035 +:10104000755802007B58020069C0000069C00000AA +:1010500069C0000069C0000069C0000069C00000EC +:101060008158020069C0000069C000008758020072 +:1010700069C000008D580200935802009958020080 +:1010800069C0000069C0000069C0000069C00000BC +:1010900069C0000069C0000069C0000069C00000AC +:1010A00069C000009F58020069C0000069C00000CC +:1010B00069C0000069C0000069C0000069C000008C +:1010C000A558020069C0000069C0000069C00000A6 +:1010D00069C0000069C0000069C0000069C000006C +:1010E00069C0000069C0000069C0000069C000005C +:1010F00069C0000069C0000069C0000069C000004C +:1011000069C0000069C0000000F002F824F03FFB55 +:101110000AA090E8000C82448344AAF10107DA4552 +:1011200001D124F034FBAFF2090EBAE80F0013F03E +:10113000010F18BFFB1A43F001031847584C020077 +:10114000784C02000A444FF0000C10F8013B13F0F9 +:10115000070408BF10F8014B1D1108BF10F8015B10 +:10116000641E05D010F8016B641E01F8016BF9D103 +:1011700013F0080F1EBF10F8014BAD1C0C1B09D15A +:101180006D1E58BF01F801CBFAD505E014F8016BCC +:1011900001F8016B6D1EF9D59142D6D3704700005E +:1011A0000023002400250026103A28BF78C1FBD870 +:1011B000520728BF30C148BF0B6070471FB500F011 +:1011C0003DF88DE80F001FBD1EF0040F0CBFEFF3BC +:1011D0000880EFF30980014A10470000ABBF000010 +:1011E000F0B44046494652465B460FB402A0013077 +:1011F00001B50648004700BF01BC86460FBC8046CB +:10120000894692469B46F0BC7047000009110000D9 +:101210008269034981614FF001001044704700006A +:101220002512000001B41EB400B514F0CBFE01B4C9 +:101230000198864601BC01B01EBD000024F0A4BA8E +:1012400070B51A4C054609202070A01C00F0D1F89A +:101250005920A08029462046BDE8704008F0CEB84D +:1012600008F0D7B870B50C461149097829B1A0F13A +:1012700060015E2908D3012013E0602804D06928AA +:1012800002D043F201000CE020CC0A4E94E80E009C +:1012900006EB8000A0F58050241FD0F8806E284611 +:1012A000B047206070BD012070470000080000209A +:1012B00018000020F05802003249884201D2012073 +:1012C00070470020704770B50446A0F500002E4E10 +:1012D000B0F1786F02D23444A4F500042948844266 +:1012E00001D2012500E0002500F043F848B125B9FE +:1012F000B44204D32548006808E0012070BD0020F6 +:1013000070BD002DF9D1B442F9D321488442F6D200 +:10131000F3E710B50446A0F50000B0F1786F03D2F2 +:1013200019480444A4F5000400F023F84FF080416C +:1013300030B11648006804E08C4204D2012003E07A +:1013400013488442F8D2002080F0010010BD10B58F +:1013500020B1FFF7DEFF08B1012010BD002010BD55 +:1013600010B520B1FFF7AFFF08B1012010BD00207C +:1013700010BD084808490068884201D10120704723 +:101380000020704700600200000000201C000020C8 +:101390000800002054000020BEBAFECA10B5044662 +:1013A0000021012000F03DF800210B2000F039F869 +:1013B0000421192000F035F804210D2000F031F847 +:1013C00004210E2000F02DF804210F2000F029F850 +:1013D0000421C84300F025F80621162000F021F86A +:1013E0000621152000F01DF82046FFF729FF0020F8 +:1013F00010BDB62101807047FFF732BF114870471A +:1014000010487047104A10B514680F4B0F4A083344 +:101410001A60FFF727FF0C48001D046010BD7047DD +:1014200070474907090E002804DB00F1E02080F82E +:101430000014704700F00F0000F1E02080F8141D48 +:101440007047000003F9004210050240010000014E +:10145000FE48002101604160018170472DE9F7439A +:10146000044692B091464068FFF771FF40B1606852 +:10147000FFF776FF20B9607800F00300022801D062 +:10148000012000E00020F14E30724846FFF71BFFBC +:1014900018B1102015B0BDE8F0834946012001F0D5 +:1014A0008EFE0028F6D101258DF842504FF4C05031 +:1014B000ADF84000002210A9284606F009FC0028DB +:1014C000E8D18DF842504FF428504FF00008ADF8A5 +:1014D000400047461C216846CDF81C8024F0EFF8F8 +:1014E0009DF81C0008AA20F00F00401C20F0F0001E +:1014F00010308DF81C0020788DF81D0061789DF863 +:101500001E0061F3420040F001008DF81E009DF8BE +:1015100000000AA940F002008DF800002089ADF813 +:101520003000ADF83270608907AFADF834000B972A +:10153000606810AC0E900A94684606F0BCF900286A +:10154000A8D1BDF8200030808DF8425042F601202D +:10155000ADF840009DF81E0008AA20F00600801C8F +:1015600020F001008DF81E000220ADF83000ADF82B +:10157000340013A80E900AA9684606F09CF90028CA +:1015800088D1BDF820007080311D484600F033F945 +:10159000002887D18DF8425042F6A620ADF84000D1 +:1015A0001C216846CDF81C8024F089F89DF81C00A9 +:1015B000ADF8345020F00F00401C20F0F000103047 +:1015C0008DF81C009DF81D0008AA20F0FF008DF882 +:1015D0001D009DF81E000AA920F0060040F0010041 +:1015E000801C8DF81E009DF800008DF8445040F0DE +:1015F00002008DF80000CDE90A4711A80E90ADF861 +:101600003050684606F057F9002899D1BDF82000FF +:10161000F08000203EE73EB504460820ADF800000B +:101620002046FFF750FE08B110203EBD21460120A4 +:1016300001F0C5FD0028F8D12088ADF804006088CD +:10164000ADF80600A088ADF80800E088ADF80A0003 +:101650007E4801AB6A468088002106F035FDBDF862 +:1016600000100829E1D003203EBD1FB5044600202C +:1016700002900820ADF80800CDF80CD02046FFF706 +:1016800022FE10B1102004B010BD704802AA81885B +:101690004FF6FF7006F05AFF0028F4D1BDF808108D +:1016A000082901D00320EEE7BDF800102180BDF825 +:1016B00002106180BDF80410A180BDF80610E18021 +:1016C000E1E701B582B00220ADF800005F4802AB4F +:1016D0006A464088002106F0F7FCBDF80010022998 +:1016E00000D003200EBD1CB5002100910221ADF8F1 +:1016F00000100190FFF70DFE08B110201CBD5348EB +:101700006A4641884FF6FF7006F020FFBDF80010D2 +:101710000229F3D003201CBDFEB54C4C06461546ED +:10172000207A0F46C00705D00846FFF7CCFD18B158 +:101730001020FEBD0F20FEBDF82D01D90C20FEBDEE +:101740003046FFF7C0FD18BB208801A905F0B8FDA1 +:101750000028F4D130788DF80500208801A906F022 +:1017600091FC0028EBD100909DF800009DF8051039 +:1017700040F002008DF80000090703D040F0080097 +:101780008DF800002088694606F019FC0028D6D1A3 +:10179000ADF8085020883B4602AA002106F094FCD0 +:1017A000BDF80810A942CAD00320FEBD7CB505468D +:1017B0000020009001900888ADF800000C462846F3 +:1017C0000195FFF7C4FD18B92046FFF7A2FD08B147 +:1017D00010207CBD15B1BDF8000050B11B486A4611 +:1017E00001884FF6FF7006F0B1FEBDF800102180B1 +:1017F0007CBD0C207CBD30B593B0044600200D4666 +:101800000090142101A823F05AFF1C2108A823F0FE +:1018100056FF9DF80000CDF808D020F00F00401CC6 +:1018200020F0F00010308DF800009DF8010020F04D +:10183000FF008DF801009DF8200040F002008DF8B7 +:10184000200001208DF8460002E000002002002068 +:1018500042F60420ADF8440011A801902088ADF8AC +:101860003C006088ADF83E00A088ADF84000E088FC +:10187000ADF842009DF8020006AA20F00600801C88 +:1018800020F001008DF802000820ADF80C00ADF842 +:1018900010000FA8059001A908A806F00CF8002870 +:1018A00003D1BDF818002880002013B030BD00001F +:1018B000F0B5007B059F1E4614460D46012800D05A +:1018C000FFDF0C2030803A203880002C08D0287AA6 +:1018D000032806D0287B012800D0FFDF1720608175 +:1018E000F0BDA889FBE72DE9F04786B0144691F8D2 +:1018F0000C900E9A0D46B9F1010F0BD01021007B10 +:101900002E8A8846052807D0062833D0FFDF06B088 +:10191000BDE8F0870221F2E7E8890C2100EB4000E6 +:1019200001EB4000188033201080002CEFD0E889B4 +:10193000608100271AE00096688808F1020301AA76 +:10194000696900F084FF06EB0800801C07EB470183 +:1019500086B204EB4102BDF8040090810DF106014E +:1019600040460E3212F0D3FD7F1CBFB26089B842F0 +:10197000E1D8CCE734201080E889B9F1010F11D00B +:10198000122148430E301880002CC0D0E8896081B5 +:101990004846B9F1010F00D00220207300270DF155 +:1019A000040A1FE00621ECE70096688808F10203AC +:1019B00001AA696900F04BFF06EB0800801C86B2A3 +:1019C000B9F1010F12D007EBC70004EB4000BDF8DE +:1019D0000410C18110220AF10201103023F0CEFD63 +:1019E0007F1CBFB26089B842DED890E707EB4701A1 +:1019F00004EB4102BDF80400D0810AF10201404627 +:101A0000103212F084FDEBE72DE9F0470E4688B066 +:101A100090F80CC096F80C80378AF5890C20109944 +:101A200002F10C044FF0000ABCF1030F08D0BCF126 +:101A3000040F3ED0BCF1070F7DD0FFDF08B067E791 +:101A400005EB850C00EB4C00188031200880002A43 +:101A5000F4D0A8F1060000F0FF09558125E0182117 +:101A600001A823F02CFE00977088434601AA7169F3 +:101A700000F0EDFEBDF804002080BDF80600E08017 +:101A8000BDF808002081A21C0DF10A01484612F0A1 +:101A90003EFDB9F1000F00D018B184F804A0A4F8FD +:101AA00002A007EB080087B20A346D1EADB2D6D291 +:101AB000C4E705EB850C00EB4C0018803220088051 +:101AC000002ABBD0A8F1050000F0FF09558137E0DE +:101AD00000977088434601AA716900F0B8FE9DF82E +:101AE0000600BDF80410E1802179420860F300018E +:101AF00062F34101820862F38201C20862F3C3010A +:101B0000020962F30411420962F34511820962F38A +:101B100086112171C0096071BDF80700208122463D +:101B20000DF10901484612F0F2FC18B184F802A048 +:101B3000A4F800A000E007E007EB080087B20A3431 +:101B40006D1EADB2C4D279E7A8F1020084B205FBE4 +:101B500008F000F10E0CA3F800C035230B80002A1A +:101B6000A6D055819481009783B270880E32716936 +:101B700000F06DFE62E72DE9F84F1E460A9D0C4607 +:101B800081462AB1607A00F58070D080E0891081AA +:101B900099F80C000C274FF000084FF00E0A0D28A2 +:101BA00073D2DFE800F09E070E1C28303846556AD5 +:101BB00073737300214648460095FFF779FEBDE830 +:101BC000F88F207B9146082802D0032800D0FFDF41 +:101BD000378030200AE000BFA9F80A80EFE7207BB9 +:101BE0009146042800D0FFDF378031202880B9F1EA +:101BF000000FF1D1E3E7207B9146042800D0FFDFFE +:101C000037803220F2E7207B9146022800D0FFDFA8 +:101C100037803320EAE7207B1746022800D0FFDF19 +:101C20003420A6F800A02880002FC8D0A7F80A808A +:101C3000C5E7207B1746042800D0FFDF3520A6F833 +:101C400000A02880002FBAD04046A7F80A8012E0F2 +:101C5000207B1746052802D0062800D0FFDF102081 +:101C6000308036202880002FA9D0E0897881A7F81D +:101C70000E80B9F80E00B881A1E7207B91460728B5 +:101C800000D0FFDF37803720B0E72AE04FF01200A6 +:101C900018804FF038001700288090D0E0897881B4 +:101CA000A7F80E80A7F8108099F80C000A2805D034 +:101CB0000B2809D00C280DD0FFDF80E7207B0A28F5 +:101CC00000D0FFDF01200AE0207B0B2800D0FFDFDF +:101CD000042004E0207B0C2800D0FFDF05203873AF +:101CE0006DE7FFDF6BE770B50C46054601F0ABFB17 +:101CF00020B10078222804D2082070BD43F20200EF +:101D000070BD0521284610F075FE206008B1002046 +:101D100070BD032070BD30B44880087820F00F00FB +:101D2000C01C20F0F000903001F8080B1DCA81E8BB +:101D30001D0030BC07F0E3BB2DE9FF4784B000274E +:101D40008246029707989046894612300AF0DCF9DD +:101D5000401D20F00306079828B907A95046FFF751 +:101D6000C2FF002854D1B9F1000F05D00798017BBC +:101D700019BB052504681BE098F80000092803D06A +:101D80000D2812D0FFDF46E0079903254868B0B35D +:101D9000497B42887143914239D98AB2B3B2011D5D +:101DA00010F09BFC0446078002E0079C04250834E1 +:101DB0000CB1208810B1032D29D02CE00798012107 +:101DC00012300AF0D3F9ADF80C00024602AB2946F6 +:101DD000504608F000FA070001D1A01C02900798B5 +:101DE0003A461230C8F80400A8F802A003A94046F9 +:101DF000029B0AF0C8F9D8B10A2817D200E006E021 +:101E0000DFE800F007091414100B0D14141213204E +:101E100014E6002012E6112010E608200EE643F238 +:101E200003000BE6072009E60D2007E6032005E680 +:101E3000BDF80C002346CDE900702A4650460799AC +:101E400000F015FD57B9032D08D10798B3B2417BB7 +:101E5000406871438AB2011D10F053FCB9F1000FC4 +:101E6000D7D0079981F80C90D3E72DE9FE4F914622 +:101E70001A881C468A468046FAB102AB494608F0E9 +:101E8000AAF9050019D04046A61C278810F0F6FED6 +:101E90003246072629463B46009610F004FB208870 +:101EA0002346CDE900504A465146404600F0DFFC4B +:101EB000002020800120BDE8FE8F0020FBE710B548 +:101EC00086B01C46AAB104238DF800301388ADF803 +:101ED00008305288ADF80A208A788DF80E200988DB +:101EE000ADF80C1000236A462146FFF725FF06B027 +:101EF00010BD1020FBE770B50D46052110F07AFDEE +:101F0000040000D1FFDF294604F11200BDE8704053 +:101F10000AF015B92DE9F8430D468046002607F072 +:101F2000EBFA04462878102878D2DFE800F0773BF7 +:101F30003453313112313131083131313131287975 +:101F4000001FC0B2022801D0102810D114BBFFDF3F +:101F500035E004B9FFDF0521404610F04BFD007B62 +:101F6000032806D004280BD0072828D0FFDF072637 +:101F700055E02879801FC0B2022820D050B1F6E782 +:101F80002879401FC0B2022819D0102817D0EEE7D8 +:101F900004B9FFDF13E004B9FFDF287901280ED16F +:101FA000172137E00521404610F024FD070000D13D +:101FB000FFDF07F1120140460AF09EF82CB12A46D5 +:101FC00021464046FFF7A7FE29E01321404602F0D4 +:101FD000F7FC24E004B9FFDF0521404610F00AFDBC +:101FE000060000D1FFDF694606F112000AF08EF804 +:101FF000060000D0FFDFA988172901D2172200E0D0 +:102000000A46BDF80000824202D9014602E005E01E +:102010001729C5D3404600F03AFCD0E7FFDF304631 +:10202000BDE8F883401D20F0030219B102FB01F066 +:10203000001D00E000201044704713B5009848B11F +:102040000024684610F0F3FA002C02D1F74A0099F8 +:1020500011601CBD01240020F4E72DE9F0470C4677 +:1020600015462421204623F02AFB05B9FFDFA87876 +:1020700060732888DFF8B4A3401D20F00301AF7817 +:102080008946DAF8000010F0F0FA060000D1FFDF10 +:102090004FF000082660A6F8008077B109FB07F131 +:1020A000091D0AD0DAF8000010F0DFFA060000D1AE +:1020B000FFDF6660C6F8008001E0C4F8048029886C +:1020C00004F11200BDE8F0470AF008B82DE9F04726 +:1020D000804601F112000D4681460AF015F8401DB8 +:1020E000D24F20F003026E7B14462968386810F046 +:1020F000E7FA3EB104FB06F2121D03D069683868A6 +:1021000010F0DEFA052010F01DFC0446052010F04A +:1021100021FC201A012802D1386810F09BFA4946A8 +:102120004046BDE8F04709F0EEBF70B50546052111 +:1021300010F060FC040000D1FFDF04F1120128461A +:10214000BDE8704009F0D8BF2DE9F04F91B04FF0D5 +:10215000000BADF834B0ADF804B047880C46054626 +:1021600092460521384610F045FC060000D1FFDFFD +:1021700024B1A780A4F806B0A4F808B029780922F1 +:102180000B20B2EB111F7DD12A7A04F11001382700 +:102190004FF00C084FF001090391102A73D2DFE8C9 +:1021A00002F072F2F1F07F08D2888D9F3DDBF3EEF2 +:1021B000B6B6307B022800D0FFDFA88908EBC0014B +:1021C000ADF804103021ADF83410002C25D060811A +:1021D000B5F80E9000271DE004EBC708317C88F8A5 +:1021E0000E10F189A8F80C10CDF80090688804232F +:1021F00004AA296900F02BFBBDF81010A8F81010F4 +:1022000009F10400BDF812107F1C1FFA80F9A8F82C +:102210001210BFB26089B842DED80DE1307B0228CF +:1022200000D0FFDFE98908EBC100ADF804003020E1 +:10223000ADF83400287B0A90001FC0B20F90002C2C +:10224000EBD06181B5F81090002725E0CDF8009023 +:102250006888696903AA0A9B00F0F9FA0A9804EBF6 +:10226000C70848441FFA80F908F10C0204A90F9826 +:1022700012F04DF918B188F80EB0A8F80CB0BDF8FE +:102280000C1001E0D4E0CFE0A8F81010BDF80E105B +:102290007F1CA8F81210BFB26089B842D6D8CBE034 +:1022A0000DA8009001AB224629463046FFF71BFBE4 +:1022B000C2E0307B082805D0FFDF03E0307B082830 +:1022C00000D0FFDFE8891030ADF804003620ADF80B +:1022D0003400002C3FD0A9896181F189A18127E0D8 +:1022E000307B092800D0FFDFA88900F10C01ADF890 +:1022F00004103721ADF83410002C2CD06081E8890F +:102300000090AB89688804F10C02296956E0E889DD +:102310003921103080B2ADF80400ADF83410002C33 +:1023200074D0A9896181287A0E280AD002212173EC +:10233000E989E181288A0090EB8968886969039AB4 +:102340003CE00121F3E70DA8009001AB22462946AD +:102350003046FFF759FB6FE0307B0A2800D0FFDFE3 +:102360001220ADF80400ADF834704CB3A989618136 +:10237000A4F810B0A4F80EB084F80C905CE020E053 +:1023800002E031E039E042E0307B0B2800D0FFDF93 +:10239000288AADF834701230ADF8040084B10421FD +:1023A0002173A9896181E989E181298A2182688A69 +:1023B00000902B8A688804F11202696900F047FADC +:1023C0003AE0307B0C2800D0FFDF1220ADF804008B +:1023D000ADF834703CB305212173A4F80AB0A4F819 +:1023E0000EB0A4F810B027E00DA8009001AB224673 +:1023F00029463046FFF75CFA1EE00DA8009001ABBD +:10240000224629463046FFF7B6FB15E034E03B2173 +:10241000ADF80400ADF8341074B3A4F80690A4F835 +:1024200008B084F80AB007E0FFDF05E010000020E4 +:10243000297A012917D0FFDFBDF80400AAF80000AF +:102440006CB1BDF834002080BDF804006080BDF898 +:102450003400392803D03C2801D086F80CB011B0E4 +:102460000020BDE8F08F3C21ADF80400ADF8341039 +:1024700014B1697AA172DFE7AAF80000EFE72DE94D +:10248000F84356880F46804615460521304610F021 +:10249000B1FA040000D1FFDF123400943B464146FC +:1024A00030466A6809F0A3FFBAE570B50D4605210C +:1024B00010F0A0FA040000D1FFDF294604F1120059 +:1024C000BDE8704009F02DBE70B50D46052110F035 +:1024D00091FA040000D1FFDF294604F11200BDE8A3 +:1024E000704009F04BBE70B50546052110F082FA28 +:1024F000040000D1FFDF04F1080321462846BDE8AF +:1025000070400422B1E470B50546052110F072FA5E +:10251000040000D1FFDF214628462368BDE8704053 +:102520000522A2E470B50646052110F063FA040006 +:1025300000D1FFDF04F1120009F0E6FD401D20F09C +:10254000030511E0011D008803224318214630468F +:10255000FFF78BFC00280BD0607BABB2684382B2E4 +:102560006068011D10F003F9606841880029E9D115 +:1025700070BD70B50E46054606F0BEFF040000D1E2 +:10258000FFDF0120207266726580207820F00F0046 +:10259000C01C20F0F00030302070BDE8704006F024 +:1025A000AEBF2DE9F0438BB00D461446814606A917 +:1025B000FFF799FB002814D14FF6FF7601274FF45F +:1025C00020588CB103208DF800001020ADF81000C9 +:1025D00007A8059007AA204604A911F0B7FF78B113 +:1025E00007200BB0BDE8F0830820ADF808508DF847 +:1025F0000E708DF80000ADF80A60ADF80C800CE0AC +:102600000698A17801742188C1818DF80E70ADF80B +:102610000850ADF80C80ADF80A606A4602214846C1 +:10262000069BFFF789FBDCE708B501228DF8022045 +:1026300042F60202ADF800200A4603236946FFF77E +:102640003EFC08BD08B501228DF8022042F60302C7 +:10265000ADF800200A4604236946FFF730FC08BDA8 +:1026600000B587B079B102228DF800200A88ADF854 +:1026700008204988ADF80A1000236A460521FFF7B3 +:102680005BFB07B000BD1020FBE709B1072316E490 +:102690000720704770B588B00D461446064606A957 +:1026A000FFF721FB00280ED17CB10620ADF80850C1 +:1026B0008DF80000ADF80A40069B6A460821DC81CF +:1026C0003046FFF739FB08B070BD05208DF80000DB +:1026D000ADF80850F0E700B587B059B107238DF881 +:1026E0000030ADF80820039100236A460921FFF766 +:1026F00023FBC6E71020C4E770B588B00C46064639 +:10270000002506A9FFF7EFFA0028DCD10698012181 +:10271000123009F02BFD9CB12178062921D2DFE887 +:1027200001F0200505160318801E80B2C01EE28845 +:1027300080B20AB1A3681BB1824203D90C20C2E760 +:102740001020C0E7042904D0A08850B901E0062079 +:10275000B9E7012913D0022905D004291CD0052985 +:102760002AD00720AFE709208DF800006088ADF877 +:102770000800E088ADF80A00A068039023E00A2072 +:102780008DF800006088ADF80800E088ADF80A0018 +:10279000A0680A25039016E00B208DF800006088E1 +:1027A000ADF80800A088ADF80A00E088ADF80C008C +:1027B000A0680B25049006E00C208DF800006078DE +:1027C0008DF808000C256A4629463046069BFFF71F +:1027D000B3FA78E700B587B00D228DF80020ADF888 +:1027E000081000236A461946FFF7A6FA49E700B524 +:1027F00087B071B102228DF800200A88ADF8082058 +:102800004988ADF80A1000236A460621FFF794FABA +:1028100037E7102035E770B586B0064601200D4633 +:10282000ADF808108DF80000014600236A463046D6 +:10283000FFF782FA040008D12946304605F05EFC15 +:102840000021304605F078FC204606B070BDF8B592 +:102850001C4615460E46069F10F0FEF92346FF1D46 +:10286000BCB231462A4600940FF0E9FDF8BD30B401 +:102870001146DDE902423CB1032903D0002330BCFC +:1028800008F034BB0123FAE71A8030BC704770B5FA +:102890000C460546FFF72FFB2146284605F03DFC78 +:1028A0002846BDE87040012105F046BC4FF0E0220B +:1028B0004FF400400021C2F88001BFF34F8FBFF3F7 +:1028C0006F8F1748016001601649900208607047D9 +:1028D000134900B500220A600A60124B4FF0607283 +:1028E0001A60002808BF00BD0F4A104BDFF840C037 +:1028F00001280CD002281CBFFFDF00BD03200860A8 +:102900001A604FF4000000BFCCF8000000BD0220A8 +:1029100008601A604FF04070F6E700B5FFDF00BDB9 +:1029200000F5004008F50140A002002014F5004029 +:1029300004F5014070B50B2000F0BDF9082000F04F +:10294000BAF900210B2000F0D4F90021082000F092 +:10295000D0F9F44C01256560A5600020C4F8400161 +:10296000C4F84401C4F848010B2000F0B5F9082070 +:1029700000F0B2F90B2000F091F9256070BD10B5A0 +:102980000B2000F098F9082000F095F9E5480121A6 +:1029900041608160E4490A68002AFCD10021C0F846 +:1029A0004011C0F84411C0F848110B2000F094F910 +:1029B000BDE81040082000F08FB910B50B2000F0E2 +:1029C0008BF9BDE81040082000F086B900B530B1A1 +:1029D000012806D0022806D0FFDF002000BDD34822 +:1029E00000BDD34800BDD248001D00BD70B5D1491F +:1029F0004FF000400860D04DC00BC5F80803CF4829 +:102A000000240460C5F840410820C43500F053F9A3 +:102A1000C5F83C41CA48047070BD08B5C14A0021E0 +:102A200028B1012811D002281CD0FFDF08BD4FF4C7 +:102A30008030C2F80803C2F84803BB483C3001604C +:102A4000C2F84011BDE80840D0E74FF40030C2F8AA +:102A50000803C2F84803B44840300160C2F844118A +:102A6000B3480CE04FF48020C2F80803C2F84803D2 +:102A7000AD4844300160C2F84811AD48001D0068FF +:102A8000009008BD70B516460D460446022800D9D0 +:102A9000FFDF0022A348012304F110018B4000EB6B +:102AA0008401C1F8405526B1C1F84021C0F8043373 +:102AB00003E0C0F80833C1F84021C0F8443370BDCA +:102AC0002DE9F0411D46144630B1012833D00228CB +:102AD00038D0FFDFBDE8F081891E002221F07F4160 +:102AE0001046FFF7CFFF012D23D00020944D924FC9 +:102AF000012668703E61914900203C39086002203F +:102B0000091D08608D490420303908608B483D3428 +:102B1000046008206C6000F0DFF83004C7F804039C +:102B2000082000F0BBF88349F007091F08602E70E9 +:102B3000D0E70120DAE7012B02D00022012005E0D6 +:102B40000122FBE7012B04D000220220BDE8F04166 +:102B500098E70122F9E774480068704770B500F003 +:102B6000D8F8704C0546D4F840010026012809D158 +:102B7000D4F80803C00305D54FF48030C4F8080327 +:102B8000C4F84061D4F8440101280CD1D4F80803FA +:102B9000800308D54FF40030C4F80803C4F844613A +:102BA000012012F0A9FCD4F8480101280CD1D4F876 +:102BB0000803400308D54FF48020C4F80803C4F884 +:102BC0004861022012F098FC5E48056070BD70B547 +:102BD00000F09FF85A4D0446287850B1FFF706FFE1 +:102BE000687818B10020687012F086FC55480460BF +:102BF00070BD0320F8E74FF0E0214FF40010C1F85A +:102C000000027047152000F067B84B4901200861A9 +:102C1000082000F061B848494FF47C10C1F808035F +:102C20000020024601EB8003C3F84025C3F8402191 +:102C3000401CC0B20628F5D37047410A43F609523A +:102C40005143C0F3080010FB02F000F5807001EB67 +:102C50005020704710B5430B48F2376463431B0C98 +:102C60005C020C60384C03FB0400384B4CF2F72438 +:102C700043435B0D13FB04F404EB402000F580702C +:102C80004012107008681844086010BD2C48406855 +:102C9000704729490120C1F800027047002809DB6C +:102CA00000F01F02012191404009800000F1E02066 +:102CB000C0F80011704700280DDB00F01F02012151 +:102CC00091404009800000F1E020C0F88011BFF37E +:102CD0004F8FBFF36F8F7047002809DB00F01F0292 +:102CE000012191404009800000F1E020C0F88012ED +:102CF00070474907090E002804DB00F1E02080F846 +:102D00000014704700F00F0000F1E02080F8141D5F +:102D100070470C48001F00680A4A0D49121D1160D7 +:102D20007047000000B0004004B500404081004002 +:102D300044B1004008F5014000800040408500405B +:102D40003400002014050240F7C2FFFF6F0C0100A1 +:102D5000010000010A4810B5046809490948083112 +:102D6000086012F05DFC0648001D046010BD0649B5 +:102D7000002008604FF0E0210220C1F88002704777 +:102D80001005024001000001FC1F004010B50D209D +:102D900000F077F8C4B26FF0040000F072F8C0B22F +:102DA000844200D0FFDF3E490120086010BD70B5AD +:102DB0000D2000F048F83B4C0020C4F8000101252C +:102DC000C4F804530D2000F04FF825604FF0E021C7 +:102DD0006014C1F8000170BD10B50D2000F033F88B +:102DE0003048012141600021C0F80011BDE81040C9 +:102DF0000D2000F039B82C4810B504682A492B483A +:102E0000083108602749D1F80001012804D0FFDF0C +:102E10002548001D046010BD2148001D00680022E7 +:102E2000C0B2C1F8002113F047F8F1E710B51D4812 +:102E3000D0F800110029FBD0FFF7DDFFBDE81040FE +:102E40000D2000F011B800280DDB00F01F02012159 +:102E500091404009800000F1E020C0F88011BFF3EC +:102E60004F8FBFF36F8F7047002809DB00F01F0200 +:102E7000012191404009800000F1E020C0F880125B +:102E80007047002804DB00F1E02090F8000405E022 +:102E900000F00F0000F1E02090F8140D4009704799 +:102EA00004D5004000D000401005024001000001A0 +:102EB0004FF0E0214FF00070C1F8800101F5C071C2 +:102EC000BFF34F8FBFF36F8FC1F80001384B8022E3 +:102ED00083F8002441F8800C704700B502460420B6 +:102EE000344903E001EBC0031B792BB1401EC0B293 +:102EF000F8D2FFDFFF2000BD41F8302001EBC00118 +:102F000000224A718A7101220A7100BD294A0021FA +:102F100002EBC0000171704710B50446042800D3CD +:102F2000FFDF244800EBC4042079012800D0FFDF34 +:102F30006079A179401CC0B2814200D060714FF02D +:102F4000E0214FF00070C1F8000210BD2DE9F04102 +:102F500019480568184919480831086014480426BA +:102F600090F80004134F4009154C042818D0FFDFD7 +:102F700016E0217807EBC1000279012A08D14279D5 +:102F800083799A4204D04279827157F831008047A0 +:102F90002078401CC0B22070042801D3002020708B +:102FA000761EF6B2E5D20448001D0560BDE8F0814A +:102FB00019E000E0D80500201005024001000001E2 +:102FC000500000200548064A0168914201D10021C5 +:102FD000016004490120086070470000540000208F +:102FE000BEBAFECA40E5014070B50C46054609F080 +:102FF0009BFB21462846BDE870400AF080BC704724 +:103000002CFFFFFFDBE5B15100600200B600FFFFBF +:103010008C00000069915B00935FFEEDA0843C731F +:10302000F87462145E06C0CB72F2136030B5F84DCE +:103030000446062CA9780ED2DFE804F0030E0E0E2B +:103040000509FFDF08E0022906D0FFDF04E00329BD +:1030500002D0FFDF00E0FFDFAC7030BD30B50446CA +:103060001038EB4D07280CD2DFE800F0040C060CFA +:103070000C0C0C00FFDF05E0287E112802D0FFDFDA +:1030800000E0FFDF2C7630BD2DE9F04111F0C8FBE8 +:10309000044612F0A1FD201AC5B206200FF052FC22 +:1030A000044606200FF056FC211AD94C207E122827 +:1030B00018D000200F1807200FF044FC0646072008 +:1030C0000FF048FC301A3918207E13280CD000204D +:1030D0000144A078042809D000200844281AC0B26E +:1030E000BDE8F0810120E5E70120F1E70120F4E7E8 +:1030F000C74810B590F825004108C54800F12600E2 +:1031000005D00DF018FBBDE8104006F00BB80DF02F +:10311000F3FAF8E730B50446A1F120000D460A287D +:103120004AD2DFE800F005070C1C2328353A3F445B +:10313000FFDF42E0207820283FD1FFDF3DE0B448A8 +:103140008178052939D0007E122836D020782428AD +:1031500033D0252831D023282FD0FFDF2DE0207851 +:1031600022282AD0232828D8FFDF26E0207822280A +:1031700023D0FFDF21E0207822281ED024281CD075 +:1031800026281AD0272818D0292816D0FFDF14E0C7 +:103190002078252811D0FFDF0FE0207825280CD0DB +:1031A000FFDF0AE02078252807D0FFDF05E0207840 +:1031B000282802D0FFDF00E0FFDF257030BD1FB5FB +:1031C00004466A46002001F03CFEB4B1BDF802207E +:1031D0004FF6FF700621824201D1ADF80210BDF812 +:1031E0000420824201D1ADF80410BDF808108142DC +:1031F00003D14FF44860ADF8080068460EF014F9AA +:1032000005F090FF04B010BD70B514460D4606469B +:10321000FEF759F858B90DB1A54201D90C2070BD7F +:10322000002408E056F82400FEF74DF808B11020FD +:1032300070BD641CE4B2AC42F4D3002070BD2DE933 +:10324000F04105461F4690460E4600240068FEF7F2 +:1032500087F830B9A98828680844401EFEF780F82E +:1032600008B110203CE728680028A88802D0B8429E +:1032700002D850E00028F5D0092031E72968085D20 +:10328000B8B1671CCA5D152A2ED03CDC152A3AD28B +:10329000DFE802F03912222228282A2A313139396E +:1032A00039393939393939392200085D30BB641C64 +:1032B000A4B2A242F9D833E00228DDD1A01C085CF8 +:1032C00088F80000072801D2400701D40A2007E748 +:1032D000307840F0010015E0C143C90707E001283C +:1032E00007D010E00620FBE60107A1F1805100297C +:1032F000F5D01846F4E63078810701D50B20EFE6CB +:1033000040F0020030702868005D384484B2A8881C +:10331000A04202D2B0E74FF4485382B2A242ADD8E5 +:103320000020DDE610B5027843F2022354080122A2 +:10333000022C12D003DC3CB1012C16D106E0032C88 +:1033400010D07F2C11D112E0002011E080790324ED +:10335000B4EB901F09D10A700BE08079B2EB901F9B +:1033600003D1F8E780798009F5D0184610BDFF2019 +:103370000870002010BD08B500208DF8000024481A +:1033800090F82E1049B190F82F0002280ED0032893 +:103390000ED0FFDF9DF8000008BD1D4869462530AE +:1033A00001F09EFD0028F5D0FFDFF3E7032000E0E9 +:1033B00001208DF80000EDE738B50C46054669465A +:1033C00001F08EFD00280DD19DF80010207861F3EA +:1033D0004700207055F8010FC4F80100A888A4F830 +:1033E0000500002038BD38B51378A8B1022813D0E5 +:1033F000FF281AD007A46D46246800944C7905EB89 +:103400009414247864F347031370032809D00FE061 +:10341000EC0100200302FF0123F0FE0313700228D9 +:10342000F3D1D8B240F0010005E043F0FE00107087 +:10343000107820F0010010700868C2F80100888838 +:10344000A2F8050038BD02210FF0D4BA38B50C46F9 +:103450000978222901D2082038BDADF800008DF886 +:10346000022068460DF0A9F905F05CFE050003D1C5 +:1034700021212046FFF74EFE284638BD1CB500200E +:103480008DF80000CDF80100ADF80500FB4890F87C +:103490002E00022801D0012000E000208DF8070056 +:1034A00068460DF0FAFA002800D0FFDF1CBD0022AC +:1034B0000A80437892B263F3451222F040020A80F8 +:1034C00000780C282BD2DFE800F02A06090E11162E +:1034D000191C1F220C2742F0110009E042F01D00C8 +:1034E00008800020704742F0110012E042F0100006 +:1034F00040F00200F4E742F01000F1E742F0010072 +:10350000EEE742F0010004E042F00200E8E742F09A +:10351000020040F00400E3E742F00400E0E7072087 +:1035200070472DE9FF478AB00025BDF82C60824620 +:103530001C4691468DF81C50700703D56068FDF756 +:10354000C2FE68B9CD4F4FF0010897F82E0058B170 +:1035500097F82F00022807D16068FDF701FF18B126 +:1035600010200EB0BDE8F087300702D5A089802872 +:103570003ED8700705D4B9F1000F02D097F82400A7 +:10358000A0B3E07DC0F300108DF81B00627D072022 +:10359000032162B3012A2DD0022AE2D0042AE0D10D +:1035A0008DF81710F00628D4A27D07202AB3012A2F +:1035B00023D0022A24D0042AD3D18DF8191000BFB9 +:1035C0008DF81590606810B307A9FFF7ABFE0028CF +:1035D000C7D19DF81C00FF2816D0606850F8011F65 +:1035E000CDF80F108088ADF8130014E000E001E082 +:1035F0000720B6E78DF81780D4E78DF81980DFE74C +:1036000002208DF81900DBE743F20220A9E7CDF88C +:103610000F50ADF81350E07B40B9207C30B9607C8E +:1036200020B9A07C10B9E07CC00601D0062098E744 +:103630008DF800A0BDF82C00ADF80200A068019044 +:10364000A068029004F10F0001F03EFC8DF80C0020 +:10365000FFF791FE8DF80D009DF81C008DF80E000F +:103660008DF816508DF81850E07D08A900F00F0075 +:103670008DF81A0068460EF015F805F053FD70E756 +:10368000F0B58FB000258DF830508DF814508DF8BE +:10369000345006468DF828500195029503950495FF +:1036A00019B10FC901AC84E80F00744CA07805284B +:1036B00001D004280CD101986168884200D120B95A +:1036C0000398E168884203D110B108200FB0F0BD23 +:1036D000207DC00601D51F2700E0FF273B460DAA2D +:1036E00005A903A8FFF7ABFD0028EFD1A08AC10709 +:1036F00002D0C00600D4EE273B460AAA0CA901A8B6 +:10370000FFF79DFD0028E1D19DF81400C00701D00E +:103710000A20DBE7A08A410708D4A17D31B19DF8DA +:103720002810890702D043F20120CFE79DF8281026 +:10373000C90709D0400707D4208818B144F2506166 +:10374000884201D90720C1E78DF818508DF819601B +:10375000BDF80800ADF81A000198079006A80DF012 +:10376000BBFF05F0DFFC0028B0D18DF820508DF8AC +:103770002160BDF81000ADF822000398099008A858 +:103780000DF0C9FF05F0CEFC00289FD101AD241D2E +:1037900095E80F0084E80F00002097E770B586B029 +:1037A0000D46040005D0FDF7DBFD20B1102006B06A +:1037B00070BD0820FBE72078C107A98802D0FF2947 +:1037C00002D303E01F2901D20920F0E7800763D468 +:1037D000FFF75AFC38B12178C1F3C100012804D0A9 +:1037E000032802D005E01320E1E7244890F82400E4 +:1037F000C8B1C8074FF001064FF0000502D08DF8A0 +:103800000F6001E08DF80F50FFF7B5FD8DF8000057 +:1038100020786946C0F3C1008DF8010060788DF80A +:103820000250C20801D00720C1E730B3C20701D05F +:103830008DF80260820705D59DF8022042F0020251 +:103840008DF80220400705D59DF8020040F00400E5 +:103850008DF80200002022780B18C2F38002DA7083 +:1038600001EB40026388D380401CA388C0B253811F +:103870000228F0D3207A78B905E001E0EC010020BD +:103880008DF80260E6E7607A30B9A07A20B9E07A74 +:1038900010B9207BC00601D0062088E704F108009B +:1038A00001F012FB8DF80E0068460DF0BFFA05F02E +:1038B00039FC002889D18DF810608DF81150E0880E +:1038C000ADF81200ADF8145004A80DF002FB05F09D +:1038D00029FC002888D12078C00701D0152000E0FD +:1038E0001320FFF7BBFB002061E72DE9FF47022013 +:1038F000FB4E8DF804000027708EADF80600B84628 +:1039000043F202094CE001A80EF0DBFF050006D0EF +:10391000708EA8B3A6F83280ADF806803EE0039C16 +:10392000A07F01072DD504F124000090A28EBDF8E0 +:103930000800214604F1360301F05FFC050005D0C4 +:103940004D452AD0112D3CD0FFDF3AE0A07F20F07A +:103950000801E07F420862F3C711A177810861F393 +:103960000000E07794F8210000F01F0084F82000A8 +:103970002078282826D129212046FFF7CBFB21E0FB +:1039800014E040070AD5BDF8080004F10E0101F06B +:10399000B1FA05000DD04D4510D100257F1CFFB2B6 +:1039A00002200EF0CFFF401CB842ACD8052D11D03C +:1039B00008E0A07F20F00400A07703E0112D00D0E4 +:1039C000FFDF0025BDF806007086052D04D02846CF +:1039D00004B0C7E5A6F832800020F9E770B50646C6 +:1039E000FFF731FD054605F087FD040000D1FFDF3C +:1039F0006680207820F00F00801C20F0F00020303E +:103A000020700320207295F83E006072BDE870407F +:103A100005F075BD2DE9F04786B0040000D1FFDF49 +:103A20002078AF4D20F00F00801C20F0F0007030A7 +:103A3000207060680178491F1B2933D2DFE801F04C +:103A4000FE32323255FD320EFDFD42FC323232780A +:103A5000FCFCFBFA3232FCFCF9F8FC00C68830466C +:103A6000FFF7F1FC0546304607F03EF9E0B160682B +:103A7000007A85F83E0021212846FFF74BFB3046AF +:103A8000FEF753FB304603F05BFE3146012012F097 +:103A9000D3FCA87F20F01000A877FFF726FF0028AE +:103AA00000D0FFDF06B05DE5207820F0F000203088 +:103AB00020700320207266806068007A607205F0D2 +:103AC0001EFDD8E7C5882846FFF7BDFC00B9FFDF1B +:103AD00060680079012800D0FFDF6068017A06B0D5 +:103AE0002846BDE8F04707F0DEBCC6883046FFF741 +:103AF000AAFC050000D1FFDF05F001FD606831463A +:103B00000089288160684089688160688089A8810F +:103B1000012012F091FC0020A875A87F00F003009E +:103B20000228BFD1FFF7E1FE0028BBD0FFDFB9E7D5 +:103B300000790228B6D000B1FFDF05F0E0FC66682E +:103B4000B6F806A0307A361D012806D0687E814678 +:103B500005F054FA070003D101E0E878F7E7FFDF4A +:103B60000022022150460EF03CFF040000D1FFDF8E +:103B700022212046FFF7CEFA3079012800D002201A +:103B8000A17F804668F30101A177308B2081708B83 +:103B90006081B08BA08184F822908DF80880B8688D +:103BA0000090F86801906A46032150460EF019FF14 +:103BB00000B9FFDFB888ADF81000B8788DF81200B2 +:103BC00004AA052150460EF00CFF00B9FFDFB888AB +:103BD000ADF80C00F8788DF80E0003AA04215046C9 +:103BE0000EF0FFFE00B9FFDF062106F1120001F022 +:103BF0009FF940B37079800700D5FFDF7179E07DD0 +:103C000061F34700E075D6F80600A0617089A083D3 +:103C1000062106F10C0001F08BF9F0B195F82500B2 +:103C20004108607861F347006070D5F8260006E02F +:103C30003EE036E06DE055E04AE02CE040E0C4F8BC +:103C40000200688D12E0E07D20F0FE00801CE0752F +:103C5000D6F81200A061F08AD9E7607820F0FE0063 +:103C6000801C6070F068C4F80200308AE080B8F10F +:103C7000010F04D0B8F1020F05D0FFDF12E70320D7 +:103C8000FFF7D4F90EE7287E122800D0FFDF1120BD +:103C9000FFF7E4F906E706B02046BDE8F04701F07B +:103CA00035BD05F02CFC15F8300F40F0020005E0A2 +:103CB00005F025FC15F8300F40F004002870F1E6FF +:103CC000287E132809D01528D8D11620FFF7C6F969 +:103CD00006B0BDE8F04705F012BC1420F6E700007E +:103CE000EC010020A978052909D00429C6D105F0E6 +:103CF00006FC022006B0BDE8F047FFF797B900794F +:103D00000028BBD0E87801F0C6F805F0F8FB0320E6 +:103D1000F0E7287E122802D1687E01F0BCF811205D +:103D2000D4E72DE9F047054600784FF00008000978 +:103D3000DFF8C0A891460C464646012875D00228F7 +:103D400074D007280AD00A2871D0FFDFA9F80060D4 +:103D500014B1A4F800806680002003E4696801279C +:103D600004F108000A784FF0020C4FF6FF73172A8F +:103D70007ED00EDC142A32D006DC052A68D0092A4F +:103D800010D0102A75D120E0152A73D0162AF9D147 +:103D9000F8E0183A082A6CD2DFE802F0F36B6B0AFD +:103DA000CAF2DFF1C8884FF01208102621468DE1D3 +:103DB0004FF01C080A26BCB38888A0806868807908 +:103DC00020726868C0796072C0E74FF01B08142643 +:103DD00054B30320207268688088A080B6E70A790F +:103DE0003C2AB3D00D1D4FF010082C26E4B1698891 +:103DF000A180298B6182298B2182698BA182A98B69 +:103E0000E1826B790246A91D1846FFF7ECFA297981 +:103E1000002001290CD084F80FC0FF212176E06139 +:103E200020626062A06291E70FE02EE151E18CE137 +:103E3000E77320760AF1040090E80E00DAF810002B +:103E4000C4E90930C4E9071280E7A9F8006083E7F4 +:103E50002C264FF01D08002CF7D00546A380887B48 +:103E60002A880F1D60F300022A80887B400802E048 +:103E70009DE007E1BEE060F341022A80887B800874 +:103E800060F382022A80887BB91CC00860F3C302F9 +:103E90002A80B87A0011401C60F3041202F07F00FF +:103EA00028807878AA1CFFF79EFA387D05F1090270 +:103EB00007F11501FFF797FA387B01F048F82874ED +:103EC000787B01F044F86874F87EA874787AE87416 +:103ED000387F2875B87B6875388AE882DAF81C0064 +:103EE000A861B87A524697F808A0C0F34111012999 +:103EF00004D0108C504503D2824609E0FFDF10E069 +:103F0000022903D0288820F0600009E0504504D140 +:103F1000288820F06000403002E0288840F06000EF +:103F20002880A4F824A0524607F11D01A86996E054 +:103F300011264FF02008002C87D0A380686804F178 +:103F40000A02007920726868007B607269688B1DC4 +:103F500048791946FFF747FAF8E60A264FF0210894 +:103F6000002CE9D08888A080686880792072686811 +:103F7000C07960729AF8301021F004019FE065E08A +:103F80004CE06FE00B264FF02208002CD4D0C888FC +:103F9000A0806868007920726868007A00F0D7FF16 +:103FA00060726868407A00F0D2FFA072CEE61C26EC +:103FB0004FF02608002CBFD0A3806868407960725B +:103FC0006868007AA0720AF1040090E80E00DAF83E +:103FD0001000C4E90530C4E90312686800793C2880 +:103FE00003D0432803D0FFDFB0E62772AEE684F8A3 +:103FF00008C0ABE610264FF02408002C9CD088881F +:10400000A0806868807920816868807A60816868AB +:104010000089A08168688089E08197E610264FF0CA +:104020002308002C88D08888A0806868C0882081F8 +:1040300068680089608168684089A08168688089B3 +:10404000E0819AF8301021F0020138E030264FF07C +:104050002508002C85D0A38069682822496821F0B2 +:104060008DFA73E614264FF01B08002C8ED0A38027 +:10407000686800790128BAD02772DAE90710C4E924 +:10408000031063E64A46214660E0287A012803D0FF +:10409000022817D0FFDF59E610264FF01F08002C2A +:1040A00089D06888A080A8892081E8896081288AD1 +:1040B000A081688AE0819AF8301021F001018AF825 +:1040C000301043E64FF012081026688800F01DFFFC +:1040D0003CE6287AC8B3012838D0022836D0032815 +:1040E00001D0FFDF32E609264FF01108002C85D001 +:1040F0006F883846FFF7A7F990F822A0A780687A62 +:104100002072042138460EF087FC052138460EF057 +:1041100083FC002138460EF07FFC012138460EF06A +:104120007BFC032138460EF077FC022138460EF066 +:1041300073FC062138460EF06FFC072138460EF05E +:104140006BFC504600F0A7FE00E6FFE72846BDE8FE +:10415000F04701F065BC70B5012803D0052800D0F8 +:10416000FFDF70BD8DB22846FFF76DF9040000D166 +:10417000FFDF20782128F4D005F0BEF980B1017866 +:1041800021F00F01891C21F0F00110310170022192 +:10419000017245800020A075BDE8704005F0AFB900 +:1041A00021462846BDE870401322FFF74FB92DE99C +:1041B000F04116460C00804600D1FFDF307820F039 +:1041C0000F00801C20F0F0001030307020780128A3 +:1041D00004D0022818D0FFDFBDE8F0814046FFF789 +:1041E00032F9050000D1FFDF0320A87505F087F93B +:1041F00094E80F00083686E80F00FE4810F8301FDC +:1042000041F001010170E7E74046FFF71CF90500A6 +:1042100000D1FFDFA1884FF6FF700027814202D155 +:10422000E288824203D0814201D1E08840B105F0AA +:1042300066F994E80F00083686E80F00AF75CBE703 +:10424000A87D0128C8D178230022414612F04AF8FF +:104250000220A875C0E738B505460C460846FDF7AC +:1042600032F818BB203D062D4AD2DFE805F0031BCB +:10427000373C42300021052012F0B4F808B111207B +:1042800038BDA01C0DF023F904F04CFF050038D117 +:10429000002208231146052012F024F8052830D00A +:1042A000FFDF2EE06068FDF752F808B1102038BD3E +:1042B000618820886A460DF0C5FB04F033FF0500D5 +:1042C0001FD16068E8B1BDF80010018019E0A07846 +:1042D00000F0010120880DF0E6FB0EE0206801F0FF +:1042E0004BFE05460DE0207800F001000CF0EDF9E2 +:1042F00003E0618820880DF020FB04F013FFF0E755 +:104300000725284638BD70B505460C460846FDF71A +:1043100000F808B1102070BD202D07D0212D0DD040 +:10432000222D0BD0252D09D0072070BD2088A11C7F +:104330000CF0A0FABDE8704004F0F4BE062070BD99 +:10434000AC482530704708B53421AA4821F0B7F9A8 +:104350000120FEF76BFE1120FEF780FEA54968469E +:10436000263105F05FF8A3489DF8002010F8251FBE +:1043700062F3470121F001010170002141724FF405 +:104380006171A0F8071002218172FEF7B1FE00B141 +:10439000FFDFFDF75DF801F084F908BD10B50C46AC +:1043A0004021204621F069F9A07F20F00300A0778A +:1043B000202020700020A07584F8230010BD7047D5 +:1043C0002DE9FC410746FCF77EFF10B11020BDE847 +:1043D000FC81884E06F12501D6F825000090B6F83C +:1043E0002950ADF8045096F82B408DF80640384619 +:1043F000FEF7E2FF0028EAD1FEF77AFE0028E6D0B9 +:10440000009946F8251FB580B471E0E710B5044661 +:10441000FCF77FFF08B1102010BD76487549224691 +:1044200090F8250026314008FEF7DDFF002010BD82 +:104430003EB504460D460846FCF76BFF08B1102058 +:104440003EBD14B143F204003EBD6A4880780528A1 +:1044500003D0042801D008203EBD694602A80AF016 +:10446000AEFA2A4669469DF80800FEF7BCFF002018 +:104470003EBDFEB50D4604004FF0000711D00822E6 +:10448000FEF7C2FE002811D1002608E054F82600ED +:104490006946FEF747FF002808D1761CF6B2AE4207 +:1044A000F4D30CF059F810B143F20320FEBD514E85 +:1044B00086F824700CB300271BE000BF54F82700D7 +:1044C00002A9FEF72FFF00B1FFDF9DF808008DF86D +:1044D000000054F8270050F8011FCDF80110808823 +:1044E000ADF8050068460CF05CF800B1FFDF7F1CFA +:1044F000FFB2AF42E2D386F824500020FEBD2DE982 +:10450000F0478AB01546894604001ED00F4608229F +:104510002946FEF779FE002811D1002613E000BFDE +:1045200054F826006946103000F0DAFC002806D165 +:104530003FB157F82600FCF7C6FE10B110200AB0B4 +:104540000BE4761CF6B2AE42EAD30026A5F10108D0 +:104550001CE000BF06F1010A0AF0FF0712E000BFED +:1045600054F82600017C4A0854F827100B7CB2EB63 +:10457000530F05D106221130113120F0D3FF58B16D +:104580007F1CFFB2AF42EBD30AF0FF064645E1DBEA +:104590004E4624B1012003E043F20520CFE700207E +:1045A0000CF024F810B90CF02DF810B143F20420EF +:1045B000C5E774B300270DF1170828E054F8270069 +:1045C0006946103000F08CFC00B1FFDF54F8270082 +:1045D000102250F8111FCDF801108088ADF80500A9 +:1045E00054F827100DF1070020F0C8FFAEB156F8BF +:1045F000271001E0EC0100201022404620F0BEFF11 +:1046000068460BF0B3FF00B1FFDF7F1CFFB2AF4283 +:10461000D4D3FEF733FF002091E7404601F0A0FC21 +:10462000EEE730B585B00446FCF74DFE18B960687A +:10463000FCF796FE10B1102005B030BD60884AF23C +:10464000B811884206D82078F84D28B1012806D044 +:10465000022804D00720EFE7FEF74AFD18E0607853 +:10466000022804D0032802D043F20220E4E785F8B0 +:104670002F00C1B200200090ADF8040002292CD018 +:10468000032927D0FFDF68460CF055F804F04AFDF7 +:104690000028D1D1606801F056FC207858B1012083 +:1046A0008DF800000DF1010001F05AFC68460DF094 +:1046B0005EFA00B1FFDF207885F82E00FEF7DEFEFF +:1046C000608860B1A88580B20BF088FF00B1FFDF81 +:1046D0000020B1E78DF80500D5E74020FAE74FF458 +:1046E0006170EFE710B50446FCF713FE20B960686F +:1046F00038B1FCF72CFE08B1102010BD606801F045 +:104700002FFCCA4830F82C1F6180C1786170807816 +:104710002070002010BD2DE9F84314468946064656 +:10472000FCF7F7FDA0B94846FCF71AFE80B9204611 +:10473000FCF716FE60B9BD4DA878012800D13CB148 +:104740003178FF2906D049B143F20400BDE8F8836F +:104750001020FBE7012801D00420F7E7CCB305289F +:1047600011D004280FD069462046FEF7A0FE00288D +:10477000ECD1217D49B1012909D0022909D00329B1 +:1047800009D00720E2E70820E0E7024604E0012222 +:1047900002E0022200E00322804623461746002062 +:1047A0000099FEF7BEFE0028D0D1A0892880A07B0A +:1047B000E875BDF80000A882AF75BDF800100907C4 +:1047C00001D5A18931B1A1892980C00704D0032076 +:1047D00003E006E08021F7E70220FEF727FC86F8D9 +:1047E00000804946BDE8F8430020FEF749BF7CB58C +:1047F0008E4C05460E46A078022803D0032801D02F +:1048000008207CBD15B143F204007CBD07200EF0EA +:10481000A1F810B9A078032806D0FEF735FC28B11E +:10482000A078032804D009E012207CBD13207CBDB1 +:10483000304600F013FB0028F9D1E670FEF79BFD2F +:1048400009F0FAFF01208DF800008DF801008DF8C5 +:1048500002502088ADF80400E07D8DF8060068461F +:104860000DF02EF804F05EFC0028E0D1A0780328BB +:1048700004D00420FEF7DAFB00207CBDE07800F0D5 +:10488000FDFA0520F6E71CB510B143F204001CBD8B +:10489000664CA078042803D0052801D008201CBD50 +:1048A00000208DF8000001218DF801108DF8020024 +:1048B00068460DF005F804F035FC0028EFD1A0782B +:1048C000052805D05FF00200FEF7B0FB00201CBDFC +:1048D000E07800F0E0FA0320F6E72DE9FC4180469D +:1048E0000E4603250846FCF73BFD002866D14046EE +:1048F000FEF7A9FD040004D02078222804D2082065 +:1049000065E543F2020062E5A07F00F003073EB1D7 +:10491000012F0CD000203146FEF751FC0500EFD1ED +:10492000012F06D0022F1AD0FFDF28464FE50120C5 +:10493000F1E7A07D3146022801D011B107E0112036 +:1049400045E56846FCF791FE0028D9D16946404606 +:1049500006F06CFD0500E8D10120A075E5E7A07D1B +:10496000032804D1314890F83000C00701D02EB39D +:104970000EE026B1A07F40071ED4002100E00121F7 +:10498000404606F073FD0500CFD1A075002ECCD0B7 +:104990003146404600F0AEFA05461128C5D1A07F49 +:1049A0004107C2D4316844F80E1F7168616040F05D +:1049B000040020740025B8E71125B6E7102006E5AD +:1049C00070B50C460546FEF73EFD010005D02246B7 +:1049D0002846BDE87040FEF739BD43F2020070BDC5 +:1049E00010B5012807D1114B9B78012B00D011B1D4 +:1049F00043F2040010BD0BF023FEBDE8104004F0AC +:104A000091BB012300F051BA00231A46194600F069 +:104A10004CBA70B506460C460846FCF754FC18B96B +:104A20002068FCF776FC18B1102070BDEC01002066 +:104A3000F84D2A7E112A04D0132A00D33EB1082053 +:104A4000F3E721463046FEF7A9FE60B1EDE7092005 +:104A5000132A0DD0142A0BD0A188FF29E5D31520E5 +:104A6000FEF7FCFA0020D4E90012C5E90712DCE7E2 +:104A7000A1881F29D9D31320F2E71CB5E548007E91 +:104A8000132801D208201CBD00208DF800006846C4 +:104A90000CF01FFA04F046FB0028F4D11120FEF7B9 +:104AA000DDFA00201CBD2DE9F04FDFF868A3814638 +:104AB00091B09AF818009B4615460C46132803D36C +:104AC000FFF7DBFF00281FD12046FCF7FCFBE8BB0B +:104AD0002846FCF7F8FBC8BB20784FF00107C00759 +:104AE0004FF0000102D08DF83A7001E08DF83A10D5 +:104AF00020788846C0F3C1008DF8000060788DF8FA +:104B00000910C10803D0072011B0BDE8F08FB0B381 +:104B1000C10701D08DF80970810705D59DF80910EE +:104B200041F002018DF80910400705D59DF80900F4 +:104B300040F004008DF809009DF80900810703D5B5 +:104B400040F001008DF80900002000E015E06E46FD +:104B500006EB400162884A81401CA288C0B20A82EA +:104B60000328F5D32078C0F3C100012825D00328FD +:104B700023D04846FCF7A7FB28B11020C4E7FFE785 +:104B80008DF80970D8E799F80000400808D001288E +:104B900009D0022807D0032805D043F20220B3E74A +:104BA0008DF8028001E08DF80270484650F8011F30 +:104BB000CDF803108088ADF80700FEF7DCFB8DF818 +:104BC00001000021424606EB41002B88C3826B881E +:104BD0008383AB884384EB880385491CC285C9B2B3 +:104BE00082860329EFD3E088ADF83C0068460CF0DC +:104BF000B5FA002887D19AF818005546112801D037 +:104C0000082081E706200DF0A5FE38B12078C0F31A +:104C1000C100012804D0032802D006E0122073E767 +:104C200095F8240000283FF46EAFFEF72DFA022815 +:104C300001D2132068E7584600F010F900289DD1F2 +:104C400085F819B068460CF0C9FB04F06BFA040053 +:104C500094D1687E00F012F91220FEF7FFF9204689 +:104C600052E770B56B4D287E122801D00820DCE693 +:104C70000CF0B7FB04F056FA040005D1687E00F092 +:104C80000AF91120FEF7EAF92046CEE670B506468D +:104C900015460C460846FCF73CFB18B92846FCF7BD +:104CA00038FB08B11020C0E62A46214630460CF0F9 +:104CB000A9FE04F037FA0028F5D121787F29F2D136 +:104CC0000520B2E67CB505460C460846FCF7FBFA23 +:104CD00008B110207CBD2846FEF7B5FB20B1007856 +:104CE000222804D208207CBD43F202007CBD494842 +:104CF00090F83000400701D511207CBD2078C00815 +:104D000002D16078C00801D007207CBDADF800500A +:104D100020788DF8020060788DF803000220ADF84D +:104D2000040068460BF0B6FF04F0FCF97CBD70B5DA +:104D300086B014460D460646FEF785FB28B100787E +:104D4000222805D2082006B06FE643F20200FAE7F7 +:104D50002846FCF705FB20B944B12046FCF7F7FADA +:104D600008B11020EFE700202060A080294890F8CB +:104D70003000800701D51120E5E703A930460BF08C +:104D8000CCFD10B104F0CEF9DDE7ADF80060BDF860 +:104D90001400ADF80200BDF81600ADF80400BDF82F +:104DA0001000BDF81210ADF80600ADF808107DB186 +:104DB000298809B1ADF80610698809B1ADF802106B +:104DC000A98809B1ADF80810E98809B1ADF8041057 +:104DD000DCB1BDF80610814201D9081A2080BDF867 +:104DE0000210BDF81400814201D9081A6080BDF894 +:104DF0000800BDF80410BDF816200144BDF81200EB +:104E00001044814201D9081AA08068460BF044FE84 +:104E1000B8E70000EC0100201CB554490968CDE951 +:104E2000001068460CF09CF904F07CF91CBD1CB520 +:104E300000200090019068460CF092F904F072F99D +:104E40001CBD10800888508048889080C8881081D8 +:104E50008888D080002050819081704710B504462A +:104E600004F0CCF830B1407830B1204604F0EBFBD0 +:104E7000002010BD052010BD122010BD10B504F09B +:104E8000BDF8040000D1FFDF607800B9FFDF607873 +:104E9000401E607010BD10B504F0B0F8040000D1E1 +:104EA000FFDF6078401C607010BD1CB5ADF80000DD +:104EB0008DF802308DF803108DF8042068460CF050 +:104EC00064FD04F02FF91CBD0CB529A2D2E9001233 +:104ED000CDE900120079694601EB501000780CBD55 +:104EE0000278520804D0012A02D043F2022070470F +:104EF000FEF718BA1FB56A46FFF7A3FF68460CF025 +:104F0000A3FA04F00FF904B010BD70B50C0006460A +:104F10000DD0FEF798FA050000D1FFDFA6802889A2 +:104F20002081288960816889A081A889E0817CE549 +:104F300010B500231A4603E0845C2343521CD2B20E +:104F40008A42F9D30BB1002010BD012010BD00B57D +:104F500040B1012805D0022803D0032804D0FFDF88 +:104F6000002000BDFF2000BD042000BD645A0200E7 +:104F7000070605040302010010B50446FCF7A3F977 +:104F800008B1102010BD2078C0F30210042807D803 +:104F90006078072804D3A178102901D8814201D272 +:104FA000072010BDE078410706D421794A0703D4D1 +:104FB000000701D4080701D5062010BD002010BD50 +:104FC00010B513785C08837F64F3C7138377137875 +:104FD0009C08C37F64F30003C3771078C309487843 +:104FE00063F34100487013781C090B7864F347138E +:104FF0000B701378DB0863F3000048705078487139 +:1050000010BD10B5C4780B7864F300030B70C4783E +:10501000640864F341030B70C478A40864F382034A +:105020000B70C478E40864F3C3030B700379117840 +:1050300063F30001117003795B0863F341011170A0 +:1050400003799B0863F3820111700079C00860F353 +:10505000C301117010BD70B514460D46064604F02C +:105060004BFA80B10178182221F00F01891C21F040 +:10507000F001A03100F8081B214620F0C4FABDE879 +:10508000704004F03CBA29463046BDE87040132217 +:10509000FEF7DCB92DE9F047064608A8894690E8F6 +:1050A00030041F4690461421284620F008FB0021BA +:1050B000CAF80010B8F1000F03D0B9F1000F03D106 +:1050C00014E03878C00711D02068FCF722F9C0BB83 +:1050D000B8F1000F07D12068123028602068143022 +:1050E00068602068A8602168CAF8001038788007D6 +:1050F00024D56068FCF72BF918BBB9F1000F21D05B +:10510000FFF71EF90168C6F868118188A6F86C11CE +:10511000807986F86E0101F0F8FCF94FEF60626863 +:1051200062B196F8680106F2691140081032FEF784 +:105130005AF910223946606820F020FA0020BDE8B4 +:10514000F08706E0606820B1E8606068C6F8640136 +:10515000F4E71020F3E730B5054608780C4620F058 +:105160000F00401C20F0F001103121700020607011 +:1051700095F8230030B104280FD0052811D0062857 +:1051800014D0FFDF20780121B1EB101F04D295F875 +:10519000200000F01F00607030BD21F0F0002030D2 +:1051A00002E021F0F00030302070EBE721F0F00059 +:1051B0004030F9E7F0B591B0022715460C46064697 +:1051C0003A46ADF80870092103AB05F004F80490E5 +:1051D000002810D004208DF804008DF80170E03410 +:1051E000099605948DF818500AA968460FF0F2F850 +:1051F00000B1FFDF012011B0F0BD10B588B00C4642 +:105200000A99ADF80000C3B11868CDF802005868DB +:10521000CDF80600ADF80A20102203A820F0AEF960 +:1052200068460CF081F903F07DFF002803D1A17FCF +:1052300041F01001A17708B010BD0020CDF80200A8 +:10524000E6E72DE9F84F0646808A0D4680B2824691 +:10525000FEF7F9F804463078DFF8A48200274FF013 +:105260000209A8F120080F2870D2DFE800F06FF2E1 +:105270003708387D8CC8F1F0EFF35FF3F300A07FBF +:1052800000F00300022809D05FF0000080F0010167 +:1052900050460DF0AFFB050003D101E00120F5E71A +:1052A000FFDF98F85C10C90702D0D8F860000BE067 +:1052B000032105F11D0010F0E0FDD5F81D00914916 +:1052C000B0FBF1F201FB1200C5F81D0070686867C1 +:1052D000B068A8672078252800D0FFDFCAE0A07F4B +:1052E00000F00300022809D05FF0000080F0010107 +:1052F00050460DF07FFB060003D101E00120F5E7E9 +:10530000FFDF3078810702D52178252904D040F0CD +:1053100001003070BDE8F88F85F80090307F28716B +:1053200006F11D002D36C5E90206F3E7A07F00F067 +:105330000300022808D0002080F0010150460DF043 +:1053400059FB040004D102E00120F5E7A7E1FFDFEB +:105350002078C10604D5072028703D346C60D9E759 +:1053600040F008002070D5E7E07F000700D5FFDFA0 +:10537000307CB28800F0010301B05046BDE8F04F28 +:10538000092105F0B3BD04B9FFDF716821B1102216 +:1053900004F1240020F0F2F828212046FDF7BAFE9F +:1053A000A07F00F0030002280ED104F124000023A6 +:1053B00000901A4621465046FFF71FFF112807D0DC +:1053C00029212046FDF7A6FE307A84F82000A1E7C7 +:1053D000A07F000700D5FFDF14F81E0F40F0080083 +:1053E0002070E782A761E761C109607861F341003D +:1053F000014660F382016170307AE0708AE7A07F35 +:1054000000F00300022809D05FF0000080F00101E5 +:1054100050460DF0EFFA040003D101E00120F5E75A +:10542000FFDF022104F1850010F027FD0420287021 +:1054300004F5B4706860B4F88500288230481038EC +:105440007C346C61C5E9028064E703E024E15BE041 +:105450002DE015E0A07F00F00300022807D0002017 +:1054600080F0010150460DF0C5FA18B901E00120A5 +:10547000F6E7FFDF324621465046BDE8F84FEAE541 +:1054800004B9FFDF20782128A1D93079012803D180 +:10549000E07F40F00800E077324621465046FFF7B3 +:1054A000DAFD2046BDE8F84F2321FDF733BE3279FF +:1054B000AA8005F108030921504604F08CFEE8603B +:1054C00010B10520287025E7A07F00F00300022816 +:1054D00008D0002080F0010150460DF08BFA040046 +:1054E00003D101E00120F5E7FFDF04F162010223AF +:1054F0001022081F0DF005F907703179417009E796 +:105500004C02002040420F00A07F00F00300022860 +:1055100008D0002080F0010150460DF06BFA050024 +:1055200003D101E00120F5E7FFDF95F8840000F0EA +:10553000030001287AD1A07F00F00307E07F10F07C +:10554000010602D0022F04D133E095F8A000C00775 +:105550002BD0D5F8601121B395F88320087C62F335 +:1055600087000874A17FCA09D5F8601162F3410071 +:105570000874D5F8601166F300000874AEB1D5F870 +:105580006001102204F1240188351FF0F7FF287E06 +:1055900040F001002876287820F0010005F88809FD +:1055A00000E016B1022F04D02DE095F88800C00766 +:1055B00027D0D5F85C1121B395F88320087C62F3DD +:1055C00087000874A17FCA09D5F85C1162F3410015 +:1055D0000874D5F85C1166F3000008748EB1D5F834 +:1055E0005C01102204F1240188351FF0C7FF2878E0 +:1055F00040F0010005F8180B287820F0010005F8AC +:10560000A009022F44D0002000EB400005EBC000B1 +:1056100090F88800800709D595F87C00D5F86421BA +:10562000400805F17D011032FDF7DDFE8DF8009098 +:1056300095F884006A4600F003008DF8010095F8A3 +:1056400088108DF8021095F8A0008DF8030021460F +:10565000504601F043FA2078252805D0212807D0AC +:10566000FFDF2078222803D922212046FDF752FDB2 +:10567000A07F00F0030002280CD0002080F0010180 +:1056800050460DF0C9F900283FF44FAEFFDF41E668 +:105690000120B9E70120F1E7706847703AE6FFDFC3 +:1056A00038E670B5FE4C002584F85C5025660EF097 +:1056B0005EFE04F11001204603F0DAFE84F830505B +:1056C00070BD70B50D46FDF7BEFE040000D1FFDFD2 +:1056D0004FF4B87128461FF0F2FF04F1240028614E +:1056E000A07F00F00300022808D0012105F1E000AE +:1056F0000EF03EFE002800D0FFDF70BD0221F5E76E +:105700000A46014602F1E0000EF052BE70B50546B1 +:10571000406886B001780A2906D00D2933D00E29B9 +:105720002FD0FFDF06B070BD86883046FDF78BFEB8 +:10573000040000D1FFDF20782128F3D028281BD1D6 +:10574000686802210E3001F0BEF9A8B1686808212E +:10575000801D01F0B8F978B104F1240130460CF055 +:10576000B1F803F0DFFC00B1FFDF06B02046BDE872 +:1057700070402921FDF7CEBC06B0BDE8704003F0B3 +:10578000BEBE012101726868C6883046FDF75BFE27 +:10579000040000D1FFDFA07F00F00301022902D145 +:1057A00020F01000A077207821280AD06868017ABC +:1057B00009B1007980B1A07F00F00300022862D017 +:1057C000FFDFA07F00F003000228ABD1FEF78DF8C9 +:1057D0000028A7D0FFDFA5E703F091FEA17F080610 +:1057E0002BD5E07FC00705D094F8200000F01F0003 +:1057F000102820D05FF0050084F8230020782928A5 +:105800001DD02428DDD13146042010F015FE2221C0 +:105810002046FDF77FFCA07F00F00300022830D077 +:105820005FF0000080F0010130460DF0F5F800282F +:10583000C7D0FFDFC5E70620DEE70420DCE701F084 +:105840000300022808D0002080F0010130460DF04E +:10585000D1F8050003D101E00120F5E7FFDF2521A4 +:105860002046FDF757FC03208DF80000694605F13E +:10587000E0000EF094FD0228A3D00028A1D0FFDFA5 +:105880009FE70120CEE703F03AFE9AE72DE9F043C7 +:1058900087B09946164688460746FDF7D4FD0400B2 +:1058A0004BD02078222848D3232846D0E07F000719 +:1058B00043D4A07F00F00300022809D05FF000006D +:1058C00080F0010138460DF095F8050002D00CE09B +:1058D0000120F5E7A07F00F00300022805D0012198 +:1058E000002238460DF07DF805466946284601F04D +:1058F0001CF9009800B9FFDF45B10098E03505615B +:105900002078222806D0242804D007E0009900201F +:10591000086103E025212046FDF7FCFB00980121EA +:1059200041704762868001A9C0E902890EF052FDEC +:10593000022802D0002800D0FFDF07B0BDE8F083C6 +:1059400070B586B00546FDF77EFD017822291ED987 +:10595000807F00F00300022808D0002080F00101C1 +:1059600028460DF047F804002FD101E00120F5E7AB +:10597000FFDF2AE0B4F85E0004F1620630440178EB +:10598000427829B121462846FFF714FCB0B9C9E690 +:10599000ADF804200921284602AB04F01CFC03905A +:1059A0000028F4D005208DF80000694604F1E000DD +:1059B0000EF0F5FC022801D000B1FFDF0223102217 +:1059C000314604F15E000CF0D2FEB4F8600000280D +:1059D000D0D1A7E610B586B00446FDF734FD0178B6 +:1059E00022291BD9807F00F00300022808D0002064 +:1059F00080F0010120460CF0FDFF040003D101E01E +:105A00000120F5E7FFDF06208DF80000694604F16C +:105A1000E0000EF0C4FC002800D0FFDF06B010BD8F +:105A20002DE9F05F05460C460027007890460109F5 +:105A30003E4604F1080BBA4602297DD0072902D060 +:105A40000A2909D146E0686801780A2905D00D299C +:105A500030D00E292ED0FFDFBBE114271C26002CEE +:105A60006BD08088A080FDF7EEFC5FEA000900D1D2 +:105A7000FFDF99F817005A46400809F11801FDF7B1 +:105A8000B2FC6868C0892082696851F8060FC4F8C2 +:105A900012004868C4F81600A07E20F0060001E05D +:105AA0002C02002040F00100A07699F81E0040F082 +:105AB00020014DE01A270A26002CD1D0C088A080F2 +:105AC000FDF7C1FC050000D1FFDF59462846FFF76E +:105AD00042FB7EE10CB1A88BA080287A0B287DD0F8 +:105AE00006DC01287BD0022808D0032804D135E049 +:105AF0000D2875D00E2874D0FFDF6AE11E27092615 +:105B0000002CADD0A088FDF79EFC5FEA000900D113 +:105B1000FFDF287B00F003000128207A1BD020F053 +:105B200001002072297B890861F341002072297BE2 +:105B3000C90861F3820001E041E1F2E02072297BB3 +:105B4000090961F3C300207299F81E0040F040017A +:105B500089F81E103DE140F00100E2E713270D2611 +:105B6000002CAAD0A088FDF76EFC8146807F00F053 +:105B70000300022808D0002080F00101A0880CF06A +:105B800039FF050003D101E00120F5E7FFDF99F8B7 +:105B90001E0000F00302022A50D0686F817801F0E5 +:105BA00003010129217A4BD021F001012172837870 +:105BB0009B0863F3410121728378DB0863F3820160 +:105BC000217283781B0963F3C3012172037863F3A5 +:105BD00006112172437863F3C71103E061E0A9E085 +:105BE00090E0A1E0217284F809A0C178A172022A94 +:105BF00029D00279E17A62F30001E1720279520858 +:105C000062F34101E1720279920862F38201E1726A +:105C10000279D20862F3C301E1724279217B62F317 +:105C2000000121734279520862F3410121734279E4 +:105C3000920862F382012173407928E0A86FADE7F2 +:105C400041F00101B2E74279E17A62F30001E172C9 +:105C50004279520862F34101E1724279920862F39B +:105C60008201E1724279D20862F3C301E1720279E2 +:105C7000217B62F3000121730279520862F3410132 +:105C800021730279920862F3820121730079C008BE +:105C900060F3C301217399F80000232831D926212C +:105CA00040E018271026E4B3A088FDF7CCFB83461C +:105CB000807F00F00300022809D0002080F001015D +:105CC000A0880CF097FE5FEA000903D101E00120F3 +:105CD000F4E7FFDFE868A06099F8000040F00401F5 +:105CE00089F8001099F80100800708D50120207379 +:105CF0009BF8000023286CD92721584651E084F8EE +:105D00000CA066E015270F265CB1A088FDF79BFB71 +:105D1000814606225946E86808F0CBFA0120A073B4 +:105D2000A0E041E048463CE016270926E4B3287B82 +:105D300020724EE0287B19270E26ACB3C4F808A0C9 +:105D4000A4F80CA0012807D0022805D0032805D00C +:105D5000042803D0FFDF0DE0207207E0697B0428F0 +:105D600001F00F0141F0800121721ED0607A20F015 +:105D700003006072A088FDF766FB054600782128C5 +:105D800027D0232800D0FFDFA87F00F003000228DF +:105D900013D0002080F00101A0880CF03DFE2221EC +:105DA0002846FDF7B7F914E004E0607A20F003001C +:105DB000401CDEE7A8F8006010E00120EAE70CB123 +:105DC0006888A080287A68B301280AD002284FD0BA +:105DD000FFDFA8F800600CB1278066800020BDE8D6 +:105DE000F09F15270F26002CE4D0A088FDF72BFB91 +:105DF000807F00F00300022808D0002080F001011D +:105E0000A0880CF0F7FD050003D101E00120F5E7C3 +:105E1000FFDFD5F81D000622594608F04AFA84F83B +:105E20000EA0D6E717270926002CC3D0A088FDF7BF +:105E30000AFB8146807F00F00300022808D0002082 +:105E400080F00101A0880CF0D5FD050003D101E030 +:105E50000120F5E7FFDF6878800701D5022000E028 +:105E60000120207299F800002328B2D9272159E790 +:105E700019270E26002C9DD0A088FDF7E4FA5FEAD2 +:105E8000000900D1FFDFC4F808A0A4F80CA084F832 +:105E900008A0A07A40F00300A07299F81E10C9096A +:105EA00061F38200A07299F81F2099F81E1012EA7F +:105EB000D11F05D099F8201001F01F0110292BD017 +:105EC00020F00800A07299F81F10607A61F3C300F7 +:105ED0006072697A01F003010129A2D140F0040047 +:105EE000607299F81E0000F003000228E87A16D0CC +:105EF000217B60F300012173AA7A607B62F30000CA +:105F00006073EA7A520862F341012173A97A490861 +:105F100061F3410060735CE740F00800D2E7617B09 +:105F200060F300016173AA7A207B62F300002073A2 +:105F3000EA7A520862F341016173A97A490861F370 +:105F40004100207345E710B5FE4C30B101461022E8 +:105F500004F120001FF012FB012084F8300010BD76 +:105F600010B5044600F0D1FDF64920461022BDE8E8 +:105F7000104020311FF002BB70B5F24D06004FF00B +:105F8000000413D0FBF79FF908B110240CE00621A0 +:105F9000304608F075F9411C05D028665FF0010015 +:105FA00085F85C0000E00724204670BD0020F7E77C +:105FB000007810F00F0204D0012A05D0022A0CD17B +:105FC00010E0000909D10AE00009012807D00228E1 +:105FD00005D0032803D0042801D00720704708709B +:105FE000002070470620704705282AD2DFE800F01D +:105FF00003070F171F00087820F0FF001EE0087845 +:1060000020F00F00401C20F0F000103016E008785F +:1060100020F00F00401C20F0F00020300EE0087847 +:1060200020F00F00401C20F0F000303006E008782F +:1060300020F00F00401C20F0F000403008700020DD +:106040007047072070472DE9F041804688B00D4623 +:1060500000270846FBF784F9A8B94046FDF7F3F995 +:10606000040003D02078222815D104E043F2020076 +:1060700008B0BDE8F08145B9A07F410603D500F026 +:106080000300022801D01020F2E7A07FC10601D44E +:10609000010702D50DB10820EAE7E17F090701D524 +:1060A0000D20E5E700F00300022805D125B12846C0 +:1060B000FEF762FF0700DBD1A07F00F0030002289B +:1060C00008D0002080F0010140460CF093FC06004F +:1060D00002D00FE00120F5E7A07F00F003000228C6 +:1060E0000ED0002080F00101002240460CF079FC27 +:1060F000060007D0A07F00F00300022804D009E0CA +:106100000120EFE70420B3E725B12A4631462046B7 +:10611000FEF756FF6946304600F007FD009800B9CB +:10612000FFDF0099022006F1E0024870C1F82480E8 +:106130004A6100220A81A27F02F00302022A1CD0D7 +:1061400001200871287800F00102087E62F3010046 +:1061500008762A78520862F3820008762A78920834 +:1061600062F3C30008762A78D20862F30410087636 +:1061700024212046FCF7CEFF33E035B30871301DF3 +:1061800088613078400908777078C0F3400048771C +:10619000287800F00102887F62F301008877A27FEF +:1061A000D20962F382008877E27F62F3C3008877C6 +:1061B000727862F304108877A878C87701F1210219 +:1061C00028462031FEF71DFF03E00320087105205B +:1061D000087625212046FCF79DFFA07F20F0400097 +:1061E000A07701A900980EF0F5F8022801D000B1BF +:1061F000FFDF38463CE72DE9FF4F534A0D4699B083 +:106200009A4607CA0AAB002783E807001998FDF7EA +:106210001AF9060006D03078262806D008201DB0CE +:10622000BDE8F08F43F20200F9E7B07F00F0030908 +:10623000B9F1020F0AD05DB91B98FEF79DFE002848 +:10624000EDD1B07F00F00300022801D11B9890BB74 +:10625000B07F00F00300022808D0002080F0010188 +:1062600019980CF0C7FB040003D101E00120F5E709 +:10627000FFDF852D28D007DCF5B1812D1ED0822DC2 +:106280001ED0832D08D11DE0862D1FD0882D1FD054 +:10629000892D1FD08A2D1FD00F2020710F281DD0CF +:1062A00003F02AF9E0B101208DF83C00201D109088 +:1062B0002079B8B15BE111E00020EEE70120ECE7C6 +:1062C0000220EAE70320E8E70520E6E70620E4E706 +:1062D0000820E2E70920E0E70A20DEE707209EE742 +:1062E00011209CE7B9F1020F03D0A56F03D1A06F75 +:1062F00002E0656FFAE7606F804632D04FF0010030 +:1063000001904FF002000090214630461B9AFEF7A4 +:1063100057FE1B98007800F00101A87861F3010096 +:10632000A870B17FC90961F38200A870F17F61F3A1 +:10633000C300A870617861F30410A8702078400948 +:10634000287003E02C0200206C5A02006078C0F331 +:10635000400068701B988078E87000206871287190 +:1063600003E00220019001200090A87898F8021024 +:10637000C0F3C000C1F3C00108405FEA000B2DD09C +:106380005046FAF7A0FF78BBDAF80C00FAF79BFF4B +:1063900050BBDAF81C00FAF796FF28BBDAF80C00BD +:1063A000A060DAF81C00E060607898F8012042EA0A +:1063B000500100BF61F34100607098F80210C0B254 +:1063C00000EA111161F3000060700020207700994D +:1063D00006F11700022908D0012107E0607898F83B +:1063E000012002EA5001E5E732E0002104EB8101DF +:1063F00048610199701C022901D0012100E00021AF +:1064000004EB81014861A87800F00300012857D10E +:1064100098F8020000F00300012851D1B9F1020FF1 +:1064200004D02A1D691D1B98FEF7EBFD287998F80A +:10643000041008408DF83400697998F8052011405F +:106440008DF8381008433BD05046FAF73CFF08B1AE +:106450001020E4E60AF110010491B9F1020F17D0FF +:106460000846002104F18C03CDE9000304F5AE7267 +:1064700002920DAB5A462046FEF70CFE0028E8D1EA +:10648000B9F1020F08D0504608D14FF0010107E0E2 +:1064900050464FF00101E5E70498F5E74FF00001A1 +:1064A00004F1A403CDE9000304F5B072029281F077 +:1064B00001010EAB5A462046FEF7ECFD0028C8D17C +:1064C0006078800734D4A87898F80210C0F3800070 +:1064D000C1F3800108432BD0297898F800000AAA5C +:1064E000B9F1020F06D032F811204300DA4002F071 +:1064F00003070AE032F810204B00DA4012F00307DD +:1065000005D0012F0BD0022F0BD0032F07D0BBF1EA +:10651000000F0DD0012906D0042904D008E002277D +:10652000F5E70127F3E7012801D0042800D104276B +:10653000B07F40F08000B077F17F6BF30001F1771E +:106540006078800706D50320A071BBF1000F0ED143 +:10655000002028E00220022F18D0012F18D0042F8D +:1065600029D00020A071B07F20F08000B0772521D5 +:106570003046FCF7CFFD0FA904F1E0000DF00FFF4E +:1065800010B1022800D0FFDF002048E6A071DFE74D +:10659000A0710D2104F120001FF091F8207840F047 +:1065A0000200207001208DF85C0017AA314619986E +:1065B00000F094FADBE70120A071D8E72DE9F04361 +:1065C00087B09046894604460025FCF73CFF06004C +:1065D00006D03078272806D0082007B0BDE8F08321 +:1065E00043F20200F9E7B07F00F00300022809D06F +:1065F0005FF0000080F0010120460CF0FBF9040080 +:1066000003D101E00120F5E7FFDFA7795FEA090088 +:1066100005D0012821D0B9F1020F26D110E0B8F140 +:10662000000F22D1012F05D0022F05D0032F05D056 +:10663000FFDF2DE00C252BE0012529E0022527E0D6 +:106640004046FAF740FEB0B9032F0ED11022414662 +:1066500004F11D001EF092FF1AE0012F02D0022F5C +:1066600003D104E0B8F1000F12D00720B5E740468F +:10667000FAF729FE08B11020AFE7102104F11D0040 +:106680001EF0FBFF0621404607F0FAFDC4F81D008E +:106690002078252140F0020020703046FCF73AFDBA +:1066A0002078C10713D020F00100207002208DF85F +:1066B000000004F11D0002908DF804506946C330BB +:1066C0000DF06DFE022803D010B1FFDF00E025774A +:1066D000002082E730B587B00D460446FCF7B3FED4 +:1066E000A0B1807F00F00300022812D05FF000000C +:1066F00080F0010120460CF07DF904000ED0284600 +:10670000FAF7E1FD38B1102007B030BD43F20200C6 +:10671000FAE70120ECE72078400701D40820F3E7EE +:10672000294604F13D00202205461EF027FF20786F +:1067300040F01000207001070FD520F008002070F5 +:1067400007208DF80000694604F1E00001950DF086 +:1067500026FE022801D000B1FFDF0020D4E770B58B +:106760000D460646FCF76FFE18B10178272921D1A6 +:1067700002E043F2020070BD807F00F003000228B7 +:1067800008D0002080F0010130460CF033F90400FD +:1067900003D101E00120F5E7FFDFA079022809D14C +:1067A0006078C00706D02A4621463046FEF702FD33 +:1067B00010B10FE0082070BDB4F860000E280BD2B5 +:1067C00004F1620102231022081F0BF09AFF01213D +:1067D00001704570002070BD112070BD70B5064677 +:1067E00014460D460846FAF76EFD18B92046FAF72A +:1067F00090FD08B1102070BDA6F57F40FF380ED087 +:106800003046FCF720FE38B1417822464B08811C07 +:106810001846FCF7E8FD07E043F2020070BD204691 +:10682000FDF7F4FD0028F9D11021E01D0FF025FB44 +:10683000E21D294604F1170000F087F9002070BD21 +:106840002DE9F04104468AB01546884600270846DF +:10685000FAF786FD18B92846FAF782FD10B1102024 +:106860000AB006E42046FCF7EEFD060003D03078BF +:1068700027281AD102E043F20200F1E7B07F00F0CE +:106880000300022808D0002080F0010120460CF00F +:10689000B1F8040003D101E00120F5E7FFDF207823 +:1068A000400702D56078800701D40820D8E7B07F80 +:1068B00000F00300022803D0A06F03D1A16F02E013 +:1068C000606FFAE7616F407800B19DB1487810B110 +:1068D000B8F1000F0ED0ADB1EA1D06A8E16800F0D6 +:1068E00034F9102206A905F117001EF01BFE18B19D +:1068F000042707E00720B3E71022E91D04F12D006B +:106900001EF03CFEB8F1000F06D0102208F107017E +:1069100004F11D001EF032FE2078252140F0020017 +:1069200020703046FCF7F6FB2078C10715D020F028 +:106930000100207002208DF8000004F11D0002907B +:10694000103003908DF804706946B3300DF027FDC8 +:10695000022803D010B1FFDF00E0277700207FE797 +:10696000F8B515460E460746FCF76DFD040004D049 +:106970002078222804D00820F8BD43F20200F8BD98 +:10698000A07F00F00300022802D043F20500F8BD0A +:106990003046FAF798FC18B92846FAF794FC08B183 +:1069A0001020F8BD00953288B31C21463846FEF70A +:1069B00024FC112815D00028F3D1297C4A08A17F96 +:1069C00062F3C711A177297CE27F61F30002E277CD +:1069D000297C890884F82010A17F21F04001A1774B +:1069E000F8BDA17F0907FBD4D6F80200C4F8360031 +:1069F000D6F80600C4F83A003088A086102229464E +:106A000004F124001EF0BAFD287C4108E07F61F308 +:106A10004100E077297C61F38200E077287C8008E0 +:106A200084F82100A07F40F00800A0770020D3E781 +:106A300070B50D4606460BB1072070BDFCF703FD8F +:106A4000040007D02078222802D3A07F800604D437 +:106A5000082070BD43F2020070BDADB1294630463A +:106A60000AF030FF02F05EFB297C4A08A17F62F346 +:106A7000C711A177297CE27F61F30002E277297CCC +:106A8000890884F8201004E030460AF03EFF02F046 +:106A900049FBA17F21F02001A17770BD70B50D46A3 +:106AA000FCF7D1FC040005D02846FAF732FC20B1EF +:106AB000102070BD43F2020070BD29462046FEF74B +:106AC0004AFB002070BD04E010F8012B0AB1002041 +:106AD0007047491E89B2F7D20120704770B515463C +:106AE000064602F009FD040000D1FFDF207820F007 +:106AF0000F00801C20F0F000203020706680286895 +:106B0000A060BDE8704002F0FABC10B5134C94F8D8 +:106B10003000002808D104F12001A1F110000DF08F +:106B200080FC012084F8300010BD10B190F8B9202D +:106B30002AB10A4890F8350018B1002003E0B830B7 +:106B400001E0064834300860704708B50023009320 +:106B500013460A460CF049F908BD00002C0200203B +:106B600018B18178012938D101E0102070470188DF +:106B700042F60112881A914231D018DC42F6010225 +:106B8000A1EB020091422AD00CDC41B3B1F5C05F09 +:106B900025D06FF4C050081821D0A0F57060FF38E0 +:106BA0001BD11CE001281AD002280AD117E0B0F549 +:106BB000807F14D008DC012811D002280FD00328D0 +:106BC0000DD0FF2809D10AE0B0F5817F07D0A0F5EC +:106BD0008070033803D0012801D0002070470F20B7 +:106BE00070470A281FD008DC0A2818D2DFE800F016 +:106BF000191B1F1F171F231D1F21102815D008DC6C +:106C00000B2812D00C2810D00D2816D00F2806D132 +:106C10000DE011280BD084280BD087280FD003203B +:106C200070470020704705207047072070470F20ED +:106C3000704704207047062070470C20704743F2CD +:106C40000200704738B50C46050041D06946FFF791 +:106C5000AFF9002819D19DF80010607861F30200A7 +:106C600060706946681CFFF7A3F900280DD19DF8F4 +:106C70000010607861F3C5006070A978C1F341012C +:106C8000012903D0022905D0072038BD217821F041 +:106C9000200102E0217841F020012170410704D059 +:106CA000A978C90861F386106070607810F0380F19 +:106CB00007D0A978090961F3C710607010F0380F88 +:106CC00002D16078400603D5207840F04000207063 +:106CD000002038BD70B50446002008801546606865 +:106CE000FFF7B0FF002816D12089A189884211D86A +:106CF00060688078C0070AD0B1F5007F0AD840F2FA +:106D00000120B1FBF0F200FB1210288007E0B1F582 +:106D1000FF7F01D90C2070BD01F2012129800020E4 +:106D200070BD10B50478137864F300031370047811 +:106D3000640864F3410313700478A40864F38203C5 +:106D400013700478E40864F3C3031370047824090F +:106D500064F3041313700478640964F34513137027 +:106D60000078800960F38613137031B10878C10789 +:106D700001D1800701D5012000E0002060F3C71396 +:106D8000137010BD4278530702D002F0070306E0EB +:106D900012F0380F02D0C2F3C20300E001234A7898 +:106DA00063F302024A70407810F0380F02D0C0F34B +:106DB000C20005E0430702D000F0070000E0012018 +:106DC00060F3C5024A7070472DE9F04F95B00D0091 +:106DD000824612D0122128461EF04FFC4FF6FF7B50 +:106DE00005AA0121584607F066F8002426463746D2 +:106DF0004FF420586FF4205973E0102015B0BDE80F +:106E0000F08F00BF9DF81E0001280AD1BDF81C10AC +:106E100041450BD011EB09000AD001280CD0022803 +:106E20000CD0042C0ED0052C0FD10DE0012400E075 +:106E30000224BDF81A6008E0032406E00424BDF82B +:106E40001A7002E0052400E00624BDF81A1051452E +:106E500047D12C74BEB34FF0000810AA4FF0070AB8 +:106E6000CDE90282CDE900A80DF13C091023CDF84F +:106E7000109042463146584607F0D0F808BBBDF89E +:106E80003C002A46C0B210A90DF041FBC8B9AE8142 +:106E9000CFB1CDE900A80DF1080C0AAE40468CE850 +:106EA0004102132300223946584607F0B7F840B98B +:106EB000BDF83C00F11CC01EC0B22A1D0DF027FB1E +:106EC00010B103209AE70AE0BDF82900E881062CFA +:106ED00005D19DF81E00A872BDF81C002881002075 +:106EE0008CE705A806F0F3FF00288BD0FFF779FEAA +:106EF00084E72DE9F0471C46DDE90978DDF82090AC +:106F000015460E00824600D1FFDF0CB1208818B173 +:106F1000D5B11120BDE8F087022D01D0012100E09C +:106F2000002106F1140005F0B5FEA8F800000246A5 +:106F30003B462946504603F04EF9C9F8000008B90F +:106F4000A41C3C600020E5E71320E3E7F0B41446FE +:106F5000DDE904528DB1002314B1022C09D101E006 +:106F6000012306E00D7CEE0703D025F00105012387 +:106F70000D742146F0BC03F0B9BF1A80F0BC704715 +:106F80002DE9FE4F91461A881C468A468046FAB182 +:106F900002AB494603F01FF9050019D04046A61C74 +:106FA00027880BF06BFE3246072629463B460096A3 +:106FB0000BF079FA20882346CDE900504A46514625 +:106FC0004046FFF7C3FF002020800120BDE8FE8F70 +:106FD0000020FBE72DE9F04786B082460EA89046D8 +:106FE00090E8B000894604AA05A903A88DE8070027 +:106FF0001E462A4621465046FFF77BFF039901B102 +:1070000001213970002818D1F94904F1140204ABA8 +:107010000860039805998DE80700424649465046A6 +:1070200006F0EFF9A8B1092811D2DFE800F0050851 +:107030000510100A0C0C0E00002006B06AE71120A3 +:10704000FBE70720F9E70820F7E70D20F5E7032025 +:10705000F3E7BDF810100398CDE9000133462A4646 +:1070600021465046FFF772FFE6E72DE9F04389B06D +:107070000D46DDE9108781461C461646142103A8FB +:107080001EF01DFB012002218DF810108DF80C0060 +:107090008DF81170ADF8146064B1A278D20709D0F0 +:1070A0008DF81600E088ADF81A00A088ADF8180039 +:1070B000A068079008A80095CDE90110424603A9F1 +:1070C00048466B68FFF786FF09B0BDE8F083F0B56E +:1070D0008BB000240646069407940727089405A859 +:1070E0000994019400970294CDE903400D461023C2 +:1070F0002246304606F092FF78B90AA806A9019404 +:1071000000970294CDE90310BDF8143000222946FF +:10711000304606F059FD002801D0FFF762FD0BB0A4 +:10712000F0BD06F0F9BB2DE9FC410C468046002677 +:1071300002F0E2F9054620780D287DD2DFE800F064 +:10714000BC0713B325BD49496383AF959B00A8488D +:10715000006820B1417841F010014170ADE0404637 +:1071600002F0FAF9A9E0042140460BF043FC0700C5 +:1071700000D1FFDF07F11401404605F01FFDA5BB5C +:1071800013214046FDF71CFC97E0042140460BF01C +:1071900031FC070000D1FFDFE088ADF800000020DF +:1071A000B8819DF80000010704D5C00602D5A0886B +:1071B000B88105E09DF8010040067ED5A088F881E1 +:1071C00005B9FFDF22462946404601F0BDFC0226F4 +:1071D00073E0E188ADF800109DF8011009060FD5A5 +:1071E000072803D006280AD00AE024E004214046FC +:1071F0000BF000FC060000D1FFDFA088F081022622 +:10720000CDB9FFDF17E0042140460BF0F3FB070088 +:1072100000D1FFDF07F1140006F0B5FB90F0010F7D +:1072200002D1E079000648D5387C022640F0020001 +:10723000387405B9FFDF00E03EE0224629464046AB +:1072400001F082FC39E0042140460BF0D3FB017CC5 +:10725000002D01F00206C1F340016171017C21F0B3 +:1072600002010174E7D1FFDFE5E702260121404674 +:1072700002F0A4F921E0042140460BF0BBFB0546D7 +:10728000606800902089ADF80400012269464046FC +:1072900002F0B5F9287C20F0020028740DE0002DE2 +:1072A000C9D1FFDFC7E7022600214046FBF70CF9F2 +:1072B000002DC0D1FFDFBEE7FFDF3046BDE8FC8117 +:1072C0003EB50C0009D001466B4601AA002006F02D +:1072D00027FF20B1FFF785FC3EBD10203EBD0020FA +:1072E0002080A0709DF8050002A900F00700FEF7BD +:1072F0007BFE50B99DF8080020709DF8050002A99A +:10730000C0F3C200FEF770FE08B103203EBD9DF839 +:10731000080060709DF80500C109A07861F30410B1 +:10732000A0709DF80510890961F3C300A0709DF855 +:107330000410890601D5022100E0012161F3420019 +:107340009DF8001061F30000A07000203EBD70B5F4 +:10735000144606460D4651EA040005D075B10846AC +:10736000F9F7F5FF78B901E0072070BD29463046EE +:1073700006F037FF10B1BDE8704032E454B120464A +:10738000F9F7E5FF08B1102070BD21463046BDE891 +:10739000704095E7002070BD2DE9FC5F0C469046DB +:1073A0000546002701780822007A3E46B2EB111FFD +:1073B0007ED104F10A0100910A31821E4FF0020AC7 +:1073C00004F1080B0191092A73D2DFE802F0ECDF27 +:1073D00005F427277AA9CD00688804210BF00AFB61 +:1073E000060000D1FFDFB08920B152270726C2E096 +:1073F0009002002051271026002C7DD06888A080A4 +:107400000120A071A88900220099FFF7A0FF0028A1 +:1074100073D1A8892081288AE081D1E0B5F8129043 +:10742000072824D1E87B000621D5512709F1140053 +:1074300086B2002CE1D0A88900220099FFF787FFCF +:1074400000285AD16888A08084F806A0A8892081E5 +:107450000120A073288A2082A4F81290A88A0090A4 +:1074600068884B46A969019A01F04BFBA8E05027B8 +:1074700009F1120086B2002C3ED0A889002259469C +:10748000FFF765FF002838D16888A080A889E080D0 +:10749000287A072813D002202073288AE081E87B0D +:1074A000C0096073A4F81090A88A0090688801E071 +:1074B00083E080E04B4604F11202A969D4E7012081 +:1074C000EAE7B5F81290512709F1140086B2002CB2 +:1074D00066D0688804210BF08DFA83466888A08006 +:1074E000A88900220099FFF732FF00286ED184F8A6 +:1074F00006A0A889208101E052E067E00420A07383 +:10750000288A2082A4F81290A88A009068884B46A6 +:10751000A969019A01F0F5FAA989ABF80E104FE0BC +:107520006888FBF790FF0746688804210BF062FA31 +:10753000064607B9FFDF06B9FFDF687BC00702D048 +:107540005127142601E0502712264CB36888A080EA +:10755000502F06D084F806A0287B594601F0E1FAA6 +:107560002EE0287BA11DF9E7FE49A88949898142BF +:1075700005D1542706269CB16888A08020E05327B7 +:107580000BE06888A080A889E08019E06888042161 +:107590000BF030FA00B9FFDF55270826002CF0D198 +:1075A000A8F8006011E056270726002CF8D068885C +:1075B000A080002013E0FFDF02E0012808D0FFDFF9 +:1075C000A8F800600CB1278066800020BDE8FC9F11 +:1075D00057270726002CE3D06888A080687AA0711E +:1075E000EEE7401D20F0030009B14143091D01EB06 +:1075F0004000704713B5DB4A00201071009848B175 +:10760000002468460BF013F8002C02D1D64A0099EA +:1076100011601CBD01240020F4E770B50D4606463C +:1076200086B014465C2128461EF049F804B9FFDFF5 +:10763000A0786874A2782188284601F09CFA00207E +:10764000A881E881228805F11401304605F09BFAF3 +:107650006A460121304606F02EFC19E09DF8030031 +:10766000000715D5BDF806103046FFF730FD9DF830 +:107670000300BDF8061040F010008DF80300BDF8BF +:107680000300ADF81400FF233046059A06F074FDA0 +:10769000684606F01CFC0028E0D006B070BD10B5AE +:1076A0000C4601F1140005F0A5FA0146627C204663 +:1076B000BDE8104001F094BA30B50446A94891B035 +:1076C0004FF6FF75C18905AA284606F0F4FB30E0A5 +:1076D0009DF81E00A0422AD001282AD1BDF81C0026 +:1076E000B0F5205F03D042F60101884221D100208D +:1076F00002AB0AAA0CA9019083E8070007200090BA +:10770000BDF81A1010230022284606F087FC38B96D +:10771000BDF828000BAAC0B20CA90CF0F8FE10B1FD +:10772000032011B030BD9DF82E00A04201D10020F1 +:10773000F7E705A806F0CBFB0028C9D00520F0E745 +:1077400070B5054604210BF055F9040000D1FFDFA8 +:1077500004F114010C46284605F030FA214628466B +:10776000BDE8704005F031BA70B58AB00C460646E7 +:10777000FBF769FE050014D02878222827D30CB126 +:10778000A08890B101208DF80C0003208DF8100026 +:1077900000208DF8110054B1A088ADF818002068C1 +:1077A00007E043F202000AB070BD0920FBE7ADF824 +:1077B00018000590042130460BF01CF9040000D19C +:1077C000FFDF04F1140005F02CFA000701D40820B3 +:1077D000E9E701F091FE60B108A802210094CDE92B +:1077E000011095F8232003A930466368FFF7F2FBE8 +:1077F000D9E71120D7E72DE9F04FB2F802A0834670 +:1078000089B0154689465046FBF71DFE0746042100 +:1078100050460BF0EFF80026044605964FF002089C +:107820000696ADF81C6007B9FFDF04B9FFDF4146DB +:10783000504603F0C6FE50B907AA06A905A88DE870 +:1078400007004246214650466368FFF752FB454811 +:1078500007AB0660DDE9051204F11400CDF80090D5 +:10786000CDE90320CDE9013197F823205946504650 +:107870006B6805F01FFA06000AD0022E04D0032E12 +:1078800014D0042E00D0FFDF09B03046BDE8F08FE1 +:10789000BDF81C000028F7D00599CDE9001042463C +:1078A000214650466368FFF751FBEDE7687840F0EA +:1078B00008006870E8E72DE9F04F99B004464FF0F2 +:1078C00000082848ADF81C80ADF82080ADF8248071 +:1078D000A0F80880ADF81480ADF81880ADF82C80C1 +:1078E000ADF82880007916460D464746012808D095 +:1078F000022806D0032804D0042802D0082019B09A +:10790000C4E72046F9F7DFFC80BB2846F9F7DBFC2B +:1079100060BB6068F9F724FD40BB606848B16089CE +:107920002189884202D8B1F5007F01D90C20E6E711 +:1079300080460EAA06A92846FFF7CCF90028DED11A +:1079400068688078C0F34100022808D19DF81900CA +:1079500010F0380F03D02869F9F7F9FC30B905A900 +:10796000206904E0900200201400002020E0FFF7CE +:1079700069F90028C3D1206948B1607880079DF873 +:10798000150000F0380001D5F0B300E0E0BB9DF831 +:10799000140080060ED59DF8150010F0380F03D0A6 +:1079A0006068F9F7D4FC18B96068F9F7D9FC08B138 +:1079B0001020A4E70AA96069FFF744F900289ED1C6 +:1079C000606940B19DF8290000F0070101293CD110 +:1079D00010F0380F39D00BA9A069FFF733F9002850 +:1079E0008DD19DF8280080062FD49DF82C008006AC +:1079F0002BD4A06950B19DF82D0000F0070101299A +:107A000023D110F0380F00E01FE01ED0E06818B15D +:107A10000078D0B11C2818D20FAA611C2046FFF7AD +:107A200080F90121384661F30F2082468DF852100B +:107A3000B94642F603000F46ADF850000DF13F0283 +:107A400018A928680CF082FD08B1072057E79DF8B7 +:107A5000600015A9CDF80090C01CCDE9019100F09F +:107A6000FF0B00230BF20122514614A806F066F921 +:107A7000F0BBBDF854000C90FD492A89286900929A +:107A8000CDE901016B89BDF838202868069906F018 +:107A900055F901007ED120784FF0020AC10601D4C9 +:107AA00080062BD5ADF80C90606950B90AA906A8DC +:107AB000FFF768F99DF8290020F00700401C8DF8B9 +:107AC00029009DF8280008A940F0C8008DF828007A +:107AD0008DF8527042F60210ADF8500003AACDF8AE +:107AE00000A0CDE90121002340F2032214A800E008 +:107AF0001EE00A9906F022F901004BD1DC484D4600 +:107B000008385B460089ADF83D000FA8CDE902902A +:107B1000CDF80490CDF810904FF007090022CDF871 +:107B20000090BDF854104FF6FF7006F04DF810B1FC +:107B3000FFF757F8E3E69DF83C00000625D52946F7 +:107B4000012060F30F218DF852704FF42450ADF8EE +:107B50005000ADF80C5062789DF80C00002362F3E1 +:107B600000008DF80C006278CDF800A0520862F396 +:107B700041008DF80C0003AACDE9012540F2032253 +:107B800014A806F0DBF8010004D1606888B320690E +:107B9000A8B900E086E005A906A8FFF7F3F8607829 +:107BA000800706D49DF8150020F038008DF81500E8 +:107BB00005E09DF8140040F040008DF814008DF8A9 +:107BC000527042F60110ADF85000208940F20121B8 +:107BD000B0FBF1F201FB1202606809ABCDF8008046 +:107BE000CDE90103002314A8059906F0A7F80100C8 +:107BF00058D12078C00729D0ADF80C50A06950B9F1 +:107C00000BA906A8FFF7BEF89DF82D0020F007008D +:107C1000401C8DF82D009DF82C008DF8527040F01E +:107C200040008DF82C0042F60310ADF8500007A973 +:107C300003AACDF800A0CDE90121002340F20322E0 +:107C400014A80B9906F07AF801002BD1E06868B30C +:107C50002946012060F30F218DF8527042F604107E +:107C6000ADF85000E068002302788DF85820407885 +:107C70008DF85900E06816AA4088ADF85A00E0680F +:107C800000798DF85C00E068C088ADF85D00CDF843 +:107C90000090CDE901254FF4027214A806F04EF8C9 +:107CA000010003D00C9800F0C7FF28E670480321BC +:107CB0000838017156B100893080BDF82400708009 +:107CC000BDF82000B080BDF81C00F080002016E652 +:107CD00070B501258AB016460B46012802D002284D +:107CE00016D104E08DF80E504FF4205003E08DF8CB +:107CF0000E5042F60100ADF80C005BB10024601C90 +:107D000060F30F2404AA08A918460CF01FFC18B150 +:107D1000072048E5102046E504A99DF82020544896 +:107D2000CDE90021801E02900023214603A802F223 +:107D3000012206F003F810B1FEF753FF33E54C487B +:107D400008380EB1C1883180057100202BE5F0B5EF +:107D500093B0074601268DF83E6041F60100ADF86C +:107D60003C0012AA0FA93046FFF7B2FF002848D105 +:107D70003F4C0025083CE7B31C2102A81DF09FFCE6 +:107D80009DF808008DF83E6040F020008DF8080056 +:107D900042F60520ADF83C000E959DF83A0011958D +:107DA00020F00600801C8DF83A009DF838006A46E5 +:107DB00020F0FF008DF838009DF8390009A920F067 +:107DC000FF008DF839000420ADF82C00ADF830002C +:107DD0000EA80A9011A80D900FA80990ADF82E508A +:107DE00002A8FFF768FD00280BD1BDF800006081F4 +:107DF00000E008E0BDF80400A081401CE08125718E +:107E0000002013B0F0BD6581A581BDF84800F4E7FE +:107E10002DE9F74F1649A0B00024083917940A79C4 +:107E2000A146012A04D0022A02D0082023B02DE561 +:107E3000CA88824201D00620F8E721988A46824209 +:107E400001D10720F2E701202146ADF848004FF6A6 +:107E5000FF788DF86E0042F6020B60F30F21ADF84B +:107E60004A80ADF86CB006918DF8724002E00000D7 +:107E7000980200201CA9ADF870401391ADF8508015 +:107E800012A806F048F800252E462F460DAB072213 +:107E900012A9404606F042F878B10A285DD195B3A0 +:107EA0008EB3ADF86450ADF866609DF85E008DF855 +:107EB000144019AC012864D06BE09DF83A001FB360 +:107EC000012859D1BDF8381059451FD118A809A962 +:107ED00001940294CDE9031007200090BDF83610FC +:107EE00010230022404606F099F8B0BBBDF86000B0 +:107EF000042801D006284AD1BDF8241021988142D7 +:107F00003AD10F2092E73AE0012835D1BDF8380088 +:107F1000B0F5205F03D042F6010188422CD1BAF8B7 +:107F20000600BDF83610884201D1012700E0002785 +:107F300005B19EB1219881421ED118A809AA0194C9 +:107F40000294CDE90320072000900D461023002263 +:107F5000404606F063F800B902E02DE04E460BE023 +:107F6000BDF86000022801D0102810D1C0B217AAB5 +:107F700009A90CF0CCFA50B9BDF8369086E7052077 +:107F800054E705A917A8221D0CF0E0FA08B1032058 +:107F90004CE79DF814000023001DC2B28DF8142098 +:107FA00022980092CDE901401BA8069905F0C6FE73 +:107FB00010B902228AF80420FEF713FE36E710B546 +:107FC0000B46401E88B084B205AA00211846FEF771 +:107FD000A8FE00200DF1080C06AA05A901908CE866 +:107FE0000700072000900123002221464FF6FF7072 +:107FF00005F0EAFD0446BDF81800012800D0FFDFB7 +:108000002046FEF7EEFD08B010BDF0B5F74F044670 +:1080100087B038790E46032804D0042802D00820FF +:1080200007B0F0BD04AA03A92046FEF753FE0500E1 +:10803000F6D160688078C0F3410002280AD19DF82B +:108040000D0010F0380F05D02069F9F780F908B15C +:108050001020E5E7208905AA21698DE807006389DA +:10806000BDF810202068039905F068FE10B1FEF7F6 +:10807000B8FDD5E716B1BDF8140030800420387182 +:108080002846CDE7F8B50C0006460CD001464FF661 +:10809000FF7500236A46284606F042F828B100BF63 +:1080A000FEF79FFDF8BD1020F8BD69462046FEF79B +:1080B000C9FD0028F8D1A078314600F00103284618 +:1080C000009A06F059F8EBE730B587B01446002265 +:1080D0000DF1080C05AD01928CE82C0007220092EE +:1080E0000A46014623884FF6FF7005F06DFDBDF886 +:1080F00014102180FEF775FD07B030BD70B50D4638 +:1081000004210AF077FC040000D1FFDF294604F1C6 +:108110001400BDE8704004F07DBD70B50D4604212B +:108120000AF068FC040000D1FFDF294604F11400C6 +:10813000BDE8704004F091BD70B50D4604210AF011 +:1081400059FC040000D1FFDF294604F11400BDE80A +:10815000704004F0A9BD70B5054604210AF04AFC40 +:10816000040000D1FFDF214628462368BDE87040A7 +:108170000122FEF705BF70B5064604210AF03AFC5D +:10818000040000D1FFDF04F1140004F033FD401DB2 +:1081900020F0030511E0011D00880022431821464C +:1081A0003046FEF7EDFE00280BD0607CABB2684392 +:1081B00082B2A068011D0AF0DAFAA068418800299D +:1081C000E9D170BD70B5054604210AF013FC040026 +:1081D00000D1FFDF214628466368BDE870400222D7 +:1081E000FEF7CEBE70B50E46054601F085F90400D7 +:1081F00000D1FFDF0120207266726580207820F0B8 +:108200000F00001D20F0F00040302070BDE87040ED +:1082100001F075B910B50446012900D0FFDF2046F2 +:10822000BDE810400121FAF74FB92DE9F04F97B0A2 +:108230004FF0000A0C008346ADF814A0D04619D0C8 +:10824000E06830B1A068A8B10188ADF81410A0F8BA +:1082500000A05846FBF7F7F8070043F2020961D087 +:10826000387822285CD3042158460AF0C3FB050065 +:1082700005D103E0102017B0BDE8F08FFFDF05F156 +:10828000140004F0B7FC401D20F00306A07801287C +:1082900003D0022801D00720EDE7218807AA58461D +:1082A00005F009FE30BB07A805F011FE10BB07A8BA +:1082B00005F00DFE48B99DF82600012805D1BDF84E +:1082C0002400A0F52451023902D04FF45050D2E7D7 +:1082D000E068B0B1CDE902A00720009005AACDF872 +:1082E00004A00492A2882188BDF81430584605F0F5 +:1082F0006BFC10B1FEF775FCBDE7A168BDF814007A +:1083000008809DF81F00C00602D543F20140B2E785 +:108310000B9838B1A1780078012905D080071AD4CC +:108320000820A8E74846A6E7C007F9D002208DF844 +:108330003C00A8684FF00009A0B1697C42887143F5 +:1083400091420FD98AB2B3B2011D0AF0C6F9804634 +:10835000A0F800A006E003208DF83C00D5F80080CE +:108360004FF001099DF8200010F0380F00D1FFDF19 +:108370009DF820001E49C0F3C200084497F823105E +:1083800010F8010C884201D90F2074E72088ADF85D +:10839000400014A90095CDE90191434607220FA999 +:1083A0005846FEF717FE002891D19DF8500050B9AD +:1083B000A078012807D1687CB3B2704382B2A86864 +:1083C000011D0AF09EF9002055E770B506461546D6 +:1083D0000C460846FEF7C4FB002805D12A46214674 +:1083E0003046BDE8704073E470BD11E59002002096 +:1083F000765A020070B51E4614460D0009D044B1ED +:10840000616831B138B1FC49C988814203D0072085 +:1084100070BD102070BD2068FEF7A2FB0028F9D1C6 +:10842000324621462846BDE87040FFF744BA70B591 +:1084300015460C0006D038B1EF490989814203D0B6 +:10844000072070BD102070BD2068FEF789FB002852 +:10845000F9D129462046BDE87040D6E570B50646FC +:1084600086B00D4614461046F8F753FFD0BB60683F +:10847000F8F776FFB0BBA6F57F40FF3803D0304653 +:10848000FAF7E1FF80B128466946FEF79DFC002817 +:108490000CD19DF810100F2008293DD2DFE801F023 +:1084A00008060606060A0A0843F2020006B070BD76 +:1084B0000320FBE79DF80210012908D1BDF8001048 +:1084C000B1F5C05FF2D06FF4C052D142EED09DF84A +:1084D000061001290DD1BDF80410A1F52851062977 +:1084E00007D200E029E0DFE801F0030304030303FF +:1084F000DCE79DF80A1001290FD1BDF80810B1F58D +:10850000245FD3D0A1F60211B1F50051CED00129DC +:10851000CCD0022901D1C9E7FFDF606878B9002318 +:1085200005AA2946304605F0FBFD10B1FEF759FBC0 +:10853000BCE79DF81400800601D41020B6E76188DE +:10854000224628466368FFF7BFFDAFE72DE9F043F9 +:10855000814687B0884614461046F8F7DAFE18B10F +:10856000102007B0BDE8F083002306AA4146484624 +:1085700005F0D6FD18B100BFFEF733FBF1E79DF81B +:108580001800C00602D543F20140EAE7002507279C +:1085900005A8019500970295CDE9035062884FF632 +:1085A000FF734146484605F039FD060013D1606867 +:1085B000F8F7AFFE60B960680195CDE90250009709 +:1085C0000495238862884146484605F027FD064603 +:1085D000BDF8140020803046CEE739B1864B0A88BA +:1085E0009B899A4202D843F2030070471DE610B5FA +:1085F00086B0814C0423ADF81430638943B1A4895B +:108600008C4201D2914205D943F2030006B010BD5D +:108610000620FBE7ADF81010002100910191ADF8A4 +:10862000003002218DF8021005A9029104A90391DE +:10863000ADF812206946FFF7F8FDE7E72DE9FC47A2 +:1086400081460D460846F8F73EFE88BB4846FAF7D5 +:10865000FAFE5FEA00080AD098F80000222829D321 +:10866000042148460AF0C6F9070005D103E043F2A9 +:108670000200BDE8FC87FFDF07F1140004F0D1FA27 +:1086800006462878012803D0022804D00720F0E706 +:10869000B0070FD502E016F01C0F0BD0A8792C1DE7 +:1086A000C00709D0E08838B1A068F8F70CFE18B10F +:1086B0001020DEE70820DCE721882A780720B1F5C2 +:1086C000847F35D01EDC40F20315A1F20313A942CA +:1086D00026D00EDCB1F5807FCBD003DCF9B10129C7 +:1086E00026D1C6E7A1F58073013BC2D0012B1FD173 +:1086F00013E0012BBDD0022B1AD0032BB9D0042BD1 +:1087000016D112E0A1F20912082A11D2DFE802F014 +:108710000B04041010101004ABE7022AA9D007E0E4 +:10872000012AA6D004E0320700E0F206002AA0DA0F +:10873000CDB200F0E1FE50B198F82300CDE900057C +:10874000FA89234639464846FEF78FFC91E7112007 +:108750008FE72DE9F04F8BB01F4615460C46834638 +:108760000026FAF770FE28B10078222805D20820EA +:108770000BB081E543F20200FAE7B80801D0072008 +:10878000F6E7032F00D100274FF6FF79CCB1022D79 +:1087900073D32046F8F7E4FD30B904EB0508A8F1DF +:1087A0000100F8F7DDFD08B11020E1E7AD1EAAB227 +:1087B0002146484605F08FFD38F8021C88425CD1FE +:1087C000ADB20D49B80702D58889401C00E00120F0 +:1087D0001FFA80F8F80701D08F8900E04F4605AAFC +:1087E0004146584605F067FB4FF0070A4FF0000975 +:1087F000DCB320460BE000009002002040881028E7 +:108800003BD8361D304486B2AE4236D2A01902881B +:108810004245F3D351E000BF9DF8170002074CD545 +:1088200094B304EB0608361DB8F80230B6B2102B2C +:1088300023D89A19AA4220D8B8F8002091421CD116 +:10884000C0061CD5CDE900A90DF1080C0AAAA11992 +:1088500048468CE80700B8F800100022584605F09A +:10886000B3F920B1FEF7BDF982E726E005E0B8F8DC +:108870000200BDF82810884201D00B2078E7B8F834 +:108880000200304486B207E0FFE7C00604D5584630 +:10889000FEF71DFC002888D19DF81700BDF81A10BE +:1088A00020F010008DF81700BDF81700ADF800009B +:1088B000FF235846009A05F05FFC05A805F007FB6A +:1088C00018B9BDF81A10B942A6D9042158460AF0C1 +:1088D00091F8040000D1FFDFA2895AB1CDE900A9C7 +:1088E0004D46002321465846FEF7BFFB0028BBD16A +:1088F000A5813DE700203BE72DE9FF4F8BB01E46E9 +:1089000017000D464FF0000412D0B00802D0072027 +:108910000FB0B1E4032E00D100265DB10846F8F790 +:1089200016FD28B93888691E0844F8F710FD08B10B +:108930001020EDE7C64AB00701D5D18900E001213A +:10894000F0074FF6FF7802D0D089401E00E0404685 +:1089500086B206AA0B9805F0AEFA4FF000094FF068 +:10896000070B0DF1140A38E09DF81B00000734D501 +:10897000CDF80490CDF800B0CDF80890CDE9039A79 +:10898000434600220B9805F049FB60BB05B3BDF8D8 +:1089900014103A8821442819091D8A4230D3BDF8A1 +:1089A0001E2020F8022BBDF8142020F8022BCDE960 +:1089B00000B9CDE90290CDF810A0BDF81E10BDF8A9 +:1089C000143000220B9805F029FB08B103209FE723 +:1089D000BDF814002044001D84B206A805F077FA03 +:1089E00020B10A2806D0FEF7FCF891E7BDF81E106A +:1089F000B142B9D934B17DB13888A11C884203D2C3 +:108A00000C2085E7052083E722462946404605F0ED +:108A100062FC014628190180A41C3C80002077E7F5 +:108A200010B50446F8F775FC08B1102010BD884851 +:108A3000C0892080002010BDF0B58BB00D460646E1 +:108A4000142103A81CF03BFE01208DF80C008DF8CA +:108A5000100000208DF81100ADF814503046FAF7E0 +:108A6000F2FC48B10078222812D30421304609F0E4 +:108A7000C1FF040005D103E043F202000BB0F0BDDA +:108A8000FFDF04F11400074604F0CBF8800601D4A0 +:108A90000820F3E7207C022140F00100207409A89F +:108AA0000094CDE90110072203A930466368FEF760 +:108AB00091FA20B1217C21F001012174DEE72946E1 +:108AC0003046F9F7F2FC08A9384604F099F800B1ED +:108AD000FFDFBDF82040172C01D2172000E0204610 +:108AE000A84201D92C4602E0172C00D217242146B7 +:108AF0003046FFF712FB21463046F9F7FCF900201B +:108B0000BCE7F8B51C4615460E46069F0AF0A4F8C9 +:108B10002346FF1DBCB231462A46009409F08FFC63 +:108B2000F8BD70B50C4605460E2120461CF0A5FD8B +:108B3000002020802DB1012D01D0FFDF70BD062067 +:108B400000E00520A07170BD10B54880087813467C +:108B500020F00F00001D20F0F00080300C4608705F +:108B60001422194604F108001CF04DFD00F0C7FC6A +:108B70003748046010BD2DE9F047DFF8D890491D53 +:108B8000064621F0030117460C46D9F8000009F00B +:108B90006CFD050000D1FFDF4FF000083560A5F83F +:108BA00000802146D9F8000009F05FFD050000D1E2 +:108BB000FFDF7560A5F800807FB104FB07F1091D98 +:108BC0000BD0D9F8000009F050FD040000D1FFDF00 +:108BD000B460C4F80080BDE8F087C6F80880FAE702 +:108BE0002DE9F0411746491D21F00302194D0646B3 +:108BF00001681446286809F063FD224671682868F8 +:108C000009F05EFD3FB104FB07F2121D03D0B1680D +:108C1000286809F055FD042009F094FE044604205C +:108C200009F098FE201A012804D12868BDE8F04117 +:108C300009F010BDBDE8F08110B50C4605F007F94C +:108C400000B1FFDF2046BDE81040FDF7CABF0000BD +:108C5000900200201400002038B50C468288817BE9 +:108C600019B14189914200D90A462280C188121D5A +:108C700090B26A4608F04FFFBDF80000032800D309 +:108C80000320C1B2208801F011F838BD38B50C4678 +:108C90008288817B19B10189914200D90A462280DC +:108CA000C188121D90B26A4608F035FFBDF8000079 +:108CB000022800D30220C1B2208800F0F7FF401C38 +:108CC000C0B238BD2DE9FE4F82468B46F9481446A6 +:108CD0000BF10302D0E90010CDE9011022F00302EC +:108CE00068464FF49071009209F0A1FCF24E002CFE +:108CF00002D1F24A00999160009901440091357FB8 +:108D000005F1010504D1E8B20BF09AFB00B1FFDFD9 +:108D1000009800EB0510C01C20F0030100915CB925 +:108D2000707AB27A1044C2B200200870308C80B2DF +:108D300004F015FF00B1FFDF0098316A084400908D +:108D40002146684600F075FF80460098C01C20F060 +:108D500003000090B37AF27A717A04B1002009F02E +:108D60005CFD0099084400902146684600F0A9FF88 +:108D7000D14800273D4690F801900CE0284600F0CD +:108D80003BFF064681788088F9F74CF971786D1CB5 +:108D900000FB0177EDB24D45F0D10098C01C20F0EA +:108DA0000300009004B100203946F9F746F9009914 +:108DB000002708440090C0483D4690F801900CE020 +:108DC000284600F019FF0646C1788088FEF709FCA6 +:108DD00071786D1C00FB0177EDB24D45F0D1009824 +:108DE000C01C20F00300009004B100203946FEF7BB +:108DF00001FC00994FF0000908440090AE484D4630 +:108E000047780EE0284600F0F7FE0646807B30B13A +:108E100006F1080001F019FF727800FB02996D1C41 +:108E2000EDB2BD42EED10098C01C20F003000090CE +:108E300004B10020494601F00CFF0099084400905D +:108E40002146684600F0AFFE0098C01D20F00700E4 +:108E50000090DAF80010814204D3A0EB0B01B1F5C9 +:108E6000803F04DB4FF00408CAF8000004E0CAF8B1 +:108E70000000B8F1000F02D04046BDE8FE8F34BBC1 +:108E80008F490020009A03F083F8FBF75CFA8A48C8 +:108E900001AA00211030F8F7E1FA00B1FFDF86489F +:108EA000407FFEF754FF00B1FFDF83484FF4F671B7 +:108EB00040301CF004FC80480421403080F8E91167 +:108EC00080F8EA11062180F8EB11032101710020DE +:108ED000D3E770B5784C06464034207804EB401553 +:108EE000E078083590B9A01990F8E80100280ED074 +:108EF000A0780F2800D3FFDF202128461CF0DFFBDD +:108F0000687866F3020068700120E070284670BD42 +:108F10002DE9F04105460C460027007805219046D2 +:108F20003E46B1EB101F00D0FFDF287A50B1012878 +:108F30000ED0FFDFA8F800600CB12780668000200B +:108F4000BDE8F0810127092674B16888A08008E097 +:108F50000227142644B16888A0802869E060A88AA6 +:108F60002082287B2072E5E7A8F80060E7E730B5AB +:108F7000514C012000212070617020726072032228 +:108F8000A272E07221732174052121831F21618364 +:108F900060744CA161610A2121776077474D4FF4DD +:108FA000B06020626868C11C21F00301814200D0DA +:108FB000FFDF6868606030BD30B5404C156863689D +:108FC00010339D4202D20420136030BD3A4B5D78CD +:108FD0005A6802EB0512107051700320D0801720E0 +:108FE00090800120D0709070002090735878401CC1 +:108FF0005870606810306060002030BD70B5064663 +:109000002D480024457807E0204600F0F5FD017862 +:10901000B14204D0641CE4B2AC42F5D1002070BD72 +:10902000F7B5074608780C4610B3FFF7E7FF05468B +:10903000A7F12006202F06D0052E19D2DFE806F072 +:109040000F2B2B151A0000F0E2FD0DB1697800E03E +:109050000021401AA17880B20844FF2808D8A078DF +:1090600030B1A088022831D202E0608817282DD2C2 +:109070000720FEBD207AE0B161881729F8D3A188C6 +:109080001729F5D3A1790029F2D0E1790029EFD091 +:10909000402804D9ECE7242F18D1207A48B1618800 +:1090A0004FF6FB70814202D8A18881420ED904207C +:1090B000FEBD0BE07C5A0200AC030020180000202B +:1090C000000000206E5246357800000065B9207817 +:1090D00002AA0121FFF770FF0028E9D12078FFF7ED +:1090E0008DFF050000D1FFDF052E18D2DFE806F066 +:1090F000030B0E081100A0786870A088E8800FE0CC +:109100006088A8800CE0A078A87009E0A078E870DA +:1091100006E054F8020FA8606068E86000E0FFDF36 +:109120000020C5E71A2835D00DDC132832D2DFE83D +:1091300000F01B31203131272723252D31312931F2 +:109140003131312F0F00302802D003DC1E2821D10D +:10915000072070473A3809281CD2DFE800F0151BB9 +:109160000F1B1B1B1B1B07000020704743F2040052 +:10917000704743F202007047042070470D2070478B +:109180000F20704708207047112070471320704748 +:10919000062070470320704710B5007800F00100EA +:1091A00008F0ABFCBDE81040BCE710B5007818B182 +:1091B000012801D0072010BD08F0EFFCBDE81040E9 +:1091C000B0E710B5007800F0010008F09FFCBDE8A2 +:1091D0001040A7E70EB5017801F001018DF80010ED +:1091E000417801F001018DF801100178C1F34001CF +:1091F0008DF802104178C1F340018DF80310017819 +:1092000089088DF80410417889088DF80510817857 +:109210008DF80610C1788DF8071000798DF80800D8 +:10922000684607F095FAFFF77DFF0EBD2DE9F84F70 +:10923000DFF8F883FE4C00264FF490771FE0012002 +:1092400000F082FD0120FFF744FE05463946D8F8BC +:10925000080009F00AFA686000B9FFDF686807F0E3 +:1092600006F9B0B12846FAF7D5FB284600F072FDA2 +:1092700028B93A466968D8F8080009F021FA94F943 +:10928000E9010428DBDA022009F05CFB074600252F +:10929000A5E03A466968D8F8080009F011FAF2E743 +:1092A000B8F802104046491C89B2A8F80210B94229 +:1092B00001D3002141800221B8F8020009F09AFB95 +:1092C000002864D0B8F80200694608F088FBFFF770 +:1092D00029FF00B1FFDF9DF8000078B1B8F8020067 +:1092E00009F0CCFC5FEA000900D1FFDF484608F036 +:1092F0003AFF18B1B8F8020002F052F9B8F80200CB +:1093000009F0AAFC5FEA000900D1FFDF484608F037 +:1093100022FFE0BB0321B8F8020009F06BFB5FEA13 +:10932000000B47D1FFDF45E0DBF8100010B10078FB +:10933000FF2849D0022000F007FD0220FFF7C9FDF9 +:109340008246484609F013F8CAF8040000B9FFDF66 +:10935000DAF8040009F0DBF8002100900170B8F899 +:1093600002105046AAF8021001F01CFE484609F00F +:10937000D0F800B9FFDF504600F0ECFC18B99AF8BD +:109380000100000704D50098CBF8100012E024E09B +:10939000DBF8100038B10178491C11F0FF010170B1 +:1093A00008D1FFDF06E000221146484600F0F9FB35 +:1093B00000B9FFDF94F9EA01022805DBB8F80200E2 +:1093C00001F0B5FD0028AFD194F9E901042804DBD0 +:1093D000484609F002F900B101266D1CEDB2BD420C +:1093E00004D294F9EA010228BFF65AAF002E7FF4A6 +:1093F00022AFBDE8F84F032000F0A6BC10B58B4C9F +:10940000E06008682061AFF2DB10F9F766FD60707C +:1094100010BD87480021403801708448017085499B +:109420004160704770B505464FF080500C46D0F84B +:10943000A410491C05D1D0F8A810C9430904090C8F +:109440000BD050F8A01F01F0010129704168216084 +:109450008068A080287830B970BD062120460CF0C5 +:109460000CFD01202870607940F0C000607170BD73 +:1094700070B54FF080540D46D4F88010491C0BD1C4 +:10948000D4F88410491C07D1D4F88810491C03D1A2 +:10949000D4F88C10491C0CD0D4F880100160D4F89A +:1094A00084104160D4F888108160D4F88C10C160B9 +:1094B00002E010210CF0E1FCD4F89000401C0BD12C +:1094C000D4F89400401C07D1D4F89800401C03D174 +:1094D000D4F89C00401C09D054F8900F28606068B4 +:1094E0006860A068A860E068E86070BD2846BDE8D4 +:1094F000704010210CF0C1BC4D480079E9E470B512 +:109500004B4CE07830B3207804EB4010407A00F008 +:109510000700204490F9E801002800DCFFDF2078F4 +:10952000002504EB4010407A00F00700011991F883 +:10953000E801401E81F8E8012078401CC0B220708C +:109540000F2800D12570A078401CA0700CF08CFB77 +:10955000E57070BDFFDF70BD3EB50546032109F023 +:1095600049FA0446284609F077FB054604B9FFDFAF +:10957000206918B10078FF2800D1FFDF01AA6946F1 +:10958000284600F00EFB60B9FFDF0AE0002202A9C6 +:10959000284600F006FB00B9FFDF9DF8080000B187 +:1095A000FFDF9DF80000411E8DF80010EED220690B +:1095B0000199884201D1002020613EBD70B5054669 +:1095C000A0F57F400C46FF3800D1FFDF012C01D011 +:1095D000FFDF70BDFFF790FF040000D1FFDF2078B0 +:1095E00020F00F00401D20F0F0005030207065800A +:1095F0000020207201202073BDE870407FE72DE934 +:10960000F04116460D460746FFF776FF040000D1ED +:10961000FFDF207820F00F00401D20F0F0005030D8 +:109620002070678001202072286805E01800002063 +:10963000EC030020F81300202061A888A082267384 +:10964000BDE8F0415BE77FB5FFF7D8FC040000D12F +:10965000FFDF02A92046FFF7FFFA054603A92046CF +:10966000FFF714FB8DF800508DF80100BDF80800DD +:10967000001DADF80200BDF80C00001DADF804009F +:10968000E088ADF80600684608F01FFA002800D010 +:10969000FFDF7FBD2DE9F05FF94E8146307810B1D4 +:1096A0000820BDE8F09F4846F7F733FE08B11020C8 +:1096B000F7E7F44C207808B9FFF759FCA17A607AF3 +:1096C0004D460844C4B200F0A2FAA04207D2201AC4 +:1096D000C1B22A460020FFF76FFC0028E1D1716873 +:1096E000E848C91C002721F003017160B3463E46DB +:1096F0003D46BA463C4690F801800AE0204600F01C +:109700007BFA4178807B0E4410FB0155641CE4B267 +:109710007F1C4445F2D1C6EBC601DA4E0AEB870046 +:1097200000EB8100F17A00EB850000EB8100DBF8B3 +:1097300004105C464518012229464846FFF7C2FA44 +:10974000070012D00020FFF759FC05000BD005F1EF +:109750001300616820F00300884200D0FFDF7078BA +:10976000401E7070656038469BE7002229464846D7 +:10977000FFF7A8FA00B1FFDFD9F8000060604FF6EC +:10978000FF7060800120207000208AE72DE9F04101 +:109790000446BB4817460E46007810B10820BDE8C5 +:1097A000F0810846F7F78FFD08B11020F7E7B54DB7 +:1097B000287808B9FFF7DBFB601E1E2807D8012CAC +:1097C00022D13078FE281FD828770020E7E7A4F1BF +:1097D00020001F2805D8E0B23A463146BDE8F041E6 +:1097E0001EE4A4F140001F2805D831462046BDE8FC +:1097F000F04100F0D7BAA4F1A0001F2804D800203F +:10980000A02C03D0A12C06D00720C8E7317801F0A6 +:1098100001016977C3E731680922F82901D38B0771 +:1098200001D01046BBE76B7C03F00303012B04D18E +:109830006B8BD7339CB28C42F3D82962AFE72DE90A +:10984000F04781460E460846F7F763FD48B948469B +:10985000F7F77DFD28B909F1030020F00301494520 +:1098600002D01020BDE8F08786484FF0000A403053 +:10987000817869B14178804600EB4114083437881B +:1098800032460021204600F073FA050004D027E09C +:10989000A6F800A00520E5E7B9F1000F24D0308834 +:1098A000B84201D90C251FE0607800F00705284672 +:1098B00000F04AFA08EB0507324697F8E8014946F6 +:1098C000401C87F8E801204607F5F47700F050FACD +:1098D00005463878401E3870032000F035FA2DB167 +:1098E0000C2D01D0A6F800A02846BBE76078644E96 +:1098F00000F00701012923D002290CD0032934D01C +:10990000FFDF98F801104046491CC9B288F80110E1 +:109910000F2935D036E0616821B1000702D4608894 +:10992000FFF71AFE98F8EA014746012802D170783D +:10993000F9F7F2FA97F9EA010428E2DBFFDFE0E742 +:10994000616821B14FF49072B06808F0B9FE98F8E0 +:10995000E9014746032802D17078F9F7DDFA97F953 +:10996000E9010428CDDBFFDFCBE7C00602D5608824 +:10997000FFF7F2FD98F9EB010628C2DBFFDFC0E735 +:1099800080F801A08178491E8170617801F007019B +:1099900001EB080090F8E811491C80F8E811A3E7F2 +:1099A00070B50D460446F7F78EFC18B92846F7F750 +:1099B000B0FC08B1102070BD29462046BDE87040BB +:1099C0000AF075BD70B505460AF094FDC4B228468C +:1099D000F7F7BDFC08B1102070BD35B128782C70A8 +:1099E00018B1A04201D0072070BD2046FDF764FEEB +:1099F000052805D10AF082FD012801D0002070BDA4 +:109A00000F2070BD70B5044615460E460846F7F7A0 +:109A10005AFC18B92846F7F77CFC08B1102070BD35 +:109A2000022C03D0102C01D0092070BD2A463146EB +:109A300020460AF06CFD0028F7D0052070BD70B5F7 +:109A400014460D460646F7F73EFC38B92846F7F7A8 +:109A500060FC18B92046F7F77AFC08B1102070BDF9 +:109A60002246294630460AF071FD0028F7D007202B +:109A700070BD3EB50446F7F74CFC28B110203EBD42 +:109A800018000020AC030020684606F0C8FDFFF770 +:109A900049FB0028F3D19DF806002070BDF80800AE +:109AA0006080BDF80A00A0800020E8E770B5054698 +:109AB0000C460846F7F74BFC20B93CB12068F7F795 +:109AC00028FC08B1102070BDA08828B12146284686 +:109AD000BDE87040FDF748BE092070BD70B5054671 +:109AE0000C460846F7F7EFFB30B9681E1E2814D85D +:109AF0002046F7F7E8FB08B1102070BD042D01D90E +:109B0000072070BD05B9FFDFF84800EB850050F86D +:109B1000041C2046BDE870400847A5F120001F281E +:109B200005D821462846BDE87040FAF794BBF02DD1 +:109B300008D0F12DE4D1207808F05CF8BDE8704041 +:109B4000FFF7F0BAA068F7F7BEFB0028D4D1204693 +:109B500008F028F8F2E770B504460D460846F7F716 +:109B6000D8FB30B9601E1E2811D82846F7F7ABFB8A +:109B700008B1102070BD012C05D0022C03D0032C9D +:109B800001D0042C01D1062070BD072070BDA4F1C6 +:109B900020001F28F9D829462046BDE87040FAF772 +:109BA000B2BB08F0A7BA38B50446D148007B00F034 +:109BB0000105D9B904F034FB0DB1226800E00022A0 +:109BC000CC484178C06806F018FCCA481030C0780C +:109BD0008DF8000010B1012802D004E0012000E05F +:109BE00000208DF80000684606F093FD002D02D09D +:109BF00020682830206038BD30B5BD4D04466878F7 +:109C0000A04200D8FFDF686800EB041030BD70B5DB +:109C1000B74800252C46467807E02046FFF7ECFFC2 +:109C20004078641C2844C5B2E4B2B442F5D1284659 +:109C300070BD2DE9F0410C4607464FF0000800F0DA +:109C4000DEF80646FF2801D94FF013083868C01C1B +:109C500020F003023A6054EA080421D1A448F3B288 +:109C6000072124300CF00EFB09E0072C10D2DFE8AE +:109C700004F0060408080A0406009F4804E09F4810 +:109C800002E09F4800E09F480CF01CFB054600E006 +:109C9000FFDFA54200D0FFDF641CE4B2072CE4D351 +:109CA000386800EB06103860404678E5021D5143E5 +:109CB000452900D245210844C01CB0FBF2F0C0B2D7 +:109CC00070472DE9FC5F064689484FF000088B4637 +:109CD0004746444690F8019022E02046FFF78CFF6B +:109CE000050000D1FFDF687869463844C7B22846CE +:109CF000FEF7B2FF824601A92846FEF7C7FF0346DA +:109D0000BDF804005246001D81B2BDF80000001DE0 +:109D100080B208F0EDFE6A78641C00FB0288E4B2B1 +:109D20004C45DAD13068C01C20F003003060BBF134 +:109D3000000F00D000204246394608F0E7FE3168A7 +:109D400008443060BDE8FC9F69494031087100203B +:109D5000C870704766494031CA782AB10A7801EB69 +:109D600042110831814201D0012070470020704724 +:109D70002DE9F04106460078154600F00F0400205A +:109D80001080601E0F46052800D3FFDF57482A4683 +:109D9000183800EB8400394650F8043C3046BDE8E2 +:109DA000F041184770B50C46402802D0412806D132 +:109DB00020E0A07861780D18E178814201D9072070 +:109DC00070BD2078012801D9132070BDFF2D08D85F +:109DD0000AF026FD06460BF0FFFE301A801EA84250 +:109DE00001DA122070BD4248216881602179017337 +:109DF000002070BDBDE87040084600F02BB82DE98A +:109E0000F047DFF8EC900026344699F8090099F8FD +:109E10000A2099F801700244D5B299F80B20104439 +:109E200000F0FF0808E02046FFF7E6FE817B40785F +:109E300011FB0066641CE4B2BC42F4D199F809102D +:109E400099F80A0029442944414400B101200844FA +:109E5000304407E538B50446407800F00300012897 +:109E600003D002280BD0072038BD606858B1F7F73F +:109E700077FAD0B96068F7F76AFA20B915E0606838 +:109E8000F7F721FA88B969462046FCF791F80028CF +:109E9000EAD1607800F00300022808D19DF80000A4 +:109EA00028B16068F7F753FA08B1102038BD61890E +:109EB000F8290DD8208988420AD8607800F003027A +:109EC0000B48012A06D1D731026A89B28A4201D2EF +:109ED000092038BD94E80E0000F1100585E80E0059 +:109EE0000AB900210183002038BD00009C5A0200FD +:109EF000AC03002018000020574100001FAD0000F7 +:109F0000E92F0000334201002DE9F04107461446D5 +:109F10008846084601F022FD064608EB88001C2210 +:109F2000796802EBC0000D18688C58B1414638467C +:109F300001F01CFD014678680078C200082305F195 +:109F400020000CE0E88CA8B14146384601F015FD30 +:109F50000146786808234078C20005F1240008F023 +:109F600006FC38B1062121726681D0E90010C4E9EF +:109F7000031009E0287809280BD00520207266819B +:109F80006868E060002028702046BDE8F04101F0DC +:109F9000DBBC072020726681F4E72DE9F04116460C +:109FA0000D460746406801EB85011C2202EBC1010A +:109FB0004418204601F003FD40B10021708865F38C +:109FC0000F2160F31F4106200CF036FA09202070A3 +:109FD000324629463846BDE8F04195E72DE9F04183 +:109FE0000E46074600241C21F07816E004EB84039B +:109FF000726801EBC303D25C6AB1FFF77DFA05001A +:10A0000000D1FFDF6F802A4621463046FFF7C5FFAB +:10A010000120BDE8F081641CE4B2A042E6D8002033 +:10A02000F7E770B5064600241C21C0780AE000BF9F +:10A0300004EB8403726801EBC303D5182A782AB1B4 +:10A04000641CE4B2A042F3D8402070BD2821284609 +:10A050001BF013FB706880892881204670BD70B5A5 +:10A06000034600201C25DC780DE000BF00EB8006D5 +:10A070005A6805EBC6063244167816B1128A8A422F +:10A0800004D0401CC0B28442F0D8402070BDF0B56E +:10A09000044600201C26E5780EE000BF00EB800798 +:10A0A000636806EBC7073B441F788F4202D15B7899 +:10A0B000934204D0401CC0B28542EFD84020F0BD8E +:10A0C0000078032801D000207047012070470078F5 +:10A0D000022801D00020704701207047007807282F +:10A0E00001D000207047012070472DE9F04106465D +:10A0F00088461078F1781546884200D3FFDF2C7827 +:10A100001C27641CF078E4B2A04201D8201AC4B223 +:10A1100004EB8401706807EBC1010844017821B1A8 +:10A120004146884708B12C7073E72878A042E8D1EF +:10A13000402028706DE770B514460B880122A240BC +:10A14000134207D113430B8001230A22011D08F09B +:10A15000D8FA047070BD2DE9FF4F81B00878DDE9B1 +:10A160000E7B9A4691460E4640072CD4019808F083 +:10A1700085FD040000D1FFDF07F1040820461FFA27 +:10A1800088F107F0C4FE050000D1FFDF2046294614 +:10A190006A4608F00EF90098A0F80370A0F805A030 +:10A1A000284608F0B4F9017869F306016BF3C7118A +:10A1B000017020461FFA88F107F0ECFE00B9FFDFBE +:10A1C000019806F08CF806EB0900017F491C017725 +:10A1D00005B0BDE8F08F2DE9F84F0E469A4691463E +:10A1E0000746032108F006FC0446008DDFF8B88519 +:10A1F000002518B198F80000B0421ED1384608F08A +:10A200003DFD070000D1FFDF09F10401384689B2A6 +:10A2100007F07DFE050010D0384629466A4608F052 +:10A22000C8F8009800210A460180817006F010F9F4 +:10A230000098C01DCAF8000021E098F80000B04264 +:10A2400016D104F1260734F8341F012000FA06F96C +:10A2500011EA090F00D0FFDF2088012340EA09003E +:10A2600020800A22391D384608F066FA067006E09A +:10A27000324604F1340104F12600FFF75CFF0A21A5 +:10A2800088F800102846BDE8F88FFEB515460C4644 +:10A29000064602AB0C220621FFF79DFF002827D0BF +:10A2A0000299607812220A70801C487008224A8045 +:10A2B000A07002982988052381806988C180A988B7 +:10A2C0000181E988418100250C20CDE900050622A5 +:10A2D00021463046FFF73FFF2946002266F31F4123 +:10A2E000F02310460BF0FEFF6078801C60700120A8 +:10A2F000FEBDFEB514460D460622064602AB1146CB +:10A30000FFF769FF002812D0029B1320002118706C +:10A31000A8785870022058809C800620CDE9000162 +:10A320000246052329463046FFF715FF0120FEBDF2 +:10A330002DE9FE430C46804644E002AB0E22072185 +:10A340004046FFF748FF002841D060681C2267782C +:10A350008678BF1C06EB860102EBC1014518029806 +:10A360001421017047700A214180698A0181E98ABC +:10A370004181A9888180A9898181304601F0EEFA66 +:10A38000029905230722C8806F70042028700025D9 +:10A390000E20CDE9000521464046FFF7DCFE2946A8 +:10A3A00066F30F2168F31F41F023002206200BF013 +:10A3B00099FF6078FD49801C607062682046921C9D +:10A3C000FFF793FE606880784028B6D10120BDE891 +:10A3D000FE83FEB50D46064638E002AB0E2207218D +:10A3E0003046FFF7F8FE002835D068681C23C17896 +:10A3F00001EB810203EBC20284180298152202705D +:10A40000627842700A224280A2894281A2888281B7 +:10A41000084601F0A3FA014602988180618AC18052 +:10A42000E18A0181A088B8B10020207000210E20AF +:10A43000CDE900010523072229463046FFF78BFEB0 +:10A440006A68DB492846D21CFFF74FFE6868C0786F +:10A450004028C2D10120FEBD0620E6E72DE9FE43DB +:10A460000C46814644E0204601F093FAD0B302AB9B +:10A47000082207214846FFF7AEFE0028A7D06068F3 +:10A480001C2265780679AD1C06EB860102EBC10142 +:10A4900047180298B7F81080062101704570042112 +:10A4A0004180304601F05AFA0146029805230722FE +:10A4B000C180A0F804807D70082038700025CDE9A7 +:10A4C000000521464846FFF746FE294666F30F2160 +:10A4D00069F31F41F023002206200BF003FF607890 +:10A4E000801C60706268B3492046121DFFF7FDFDB5 +:10A4F000606801794029B6D1012068E72DE9F34F62 +:10A5000083B00E4680E0304601F043FA002875D053 +:10A5100071681C2091F8068008EB880200EBC200ED +:10A520000C184146304601F028FA0146A078C300D5 +:10A5300070684078C20004F1240008F034F907463E +:10A540008088E18B401A80B2002581B3AA46218B16 +:10A55000814200D808468146024602AB0721039893 +:10A56000FFF739FE010028D0BAF1000F03D0029A9C +:10A57000B888022510808B46E28B3968A9EB05006C +:10A580001FFA80FA0A440398009208F077FBED1D49 +:10A59000009A59465346009507F085FFE08B5044DA +:10A5A00080B2E083B988884209D1012508E0FFE73D +:10A5B000801C4FF0010A80B2C9E7002008E60025A0 +:10A5C000CDE90095238A072231460398FFF7C3FDA2 +:10A5D000E089401EE0818DB1A078401CA0707068B9 +:10A5E000F178427811FB02F1CAB2816901230E3081 +:10A5F00008F087F880F800800020E08372686E49D8 +:10A600003046921DFFF771FD7068817940297FF413 +:10A610007AAF0120DCE570B5064648680D46144661 +:10A620008179402910D104EB84011C2202EBC10185 +:10A63000084401F0E5F9002806D0686829468471CD +:10A640003046BDE8704059E770BDFEB50C46074680 +:10A65000002645E0204601F09CF9D8B360681C2232 +:10A66000417901EB810102EBC1014518688900B90C +:10A67000FFDF02AB082207213846FFF7ACFD0028B8 +:10A6800033D00299607816220A70801C487004202A +:10A6900048806068407901F061F90146029805231D +:10A6A000072281806989C1800820CDE90006214602 +:10A6B0003846FFF750FD6078801C6070A889698972 +:10A6C0000844B0F5803F00D3FFDFA88969890844BA +:10A6D000A8816E81626839492046521DFFF705FD49 +:10A6E000606841794029B5D10120FEBD30B5438C69 +:10A6F000458BC3F3C704002345B1838B641EED1A59 +:10A70000C38A6D1E1D4495FBF3F3E4B22CB100899E +:10A7100018B1A04200D8204603444FF6FF70834290 +:10A7200000D3034613800C7030BD2DE9FC41074671 +:10A7300016460D46486802EB86011C2202EBC10159 +:10A7400044186A4601A92046FFF7D0FFA089618915 +:10A7500001448AB2BDF80010914212D0081A00D507 +:10A76000002060816868407940280AD1204601F0C5 +:10A770003DF9002805D06868294646713846FFF73C +:10A7800064FFBDE8FC812DE9FE4F894680461546F1 +:10A790005088032108F02EF98346B8F802004028BB +:10A7A0000ED240200DE000002C000020C1A00000CF +:10A7B000CFA00000DDA0000001BA0000EDB900004C +:10A7C000403880B282460146584601F0E2F800283F +:10A7D0007ED00AEB8A001C22DBF8041002EBC000DA +:10A7E0000C18204601F0EBF8002877D1B8F80000EB +:10A7F000E18A88423CD8A189D1B348456ED1002670 +:10A800005146584601F0B2F8218C0F18608B48B9B8 +:10A81000B9F1020F62D3B8F804006083618A8842FC +:10A8200026D80226A9EB06001FFA80F9B888A28B69 +:10A83000801A002814DD4946814500DA084683B2B3 +:10A8400068886968029139680A44CDE9003208F0E5 +:10A8500003FADDE90121F61D009B009607F0EFFDEC +:10A86000A18B01EB090080B2A083618B884207D9DC +:10A87000688803B052465946BDE8F04F01F0DDB894 +:10A880001FD14FF009002872B8F802006881D7E99B +:10A890000001C5E90401608BA881284601F054F845 +:10A8A0005146584601F062F80146DBF804000823DF +:10A8B0000078C20004F1200007F059FF0020A083B7 +:10A8C0006083A0890AF0FF02401EA081688800E032 +:10A8D00004E003B05946BDE8F04F26E7BDE8FE8F1F +:10A8E0002DE9F041064615460F461C461846F6F778 +:10A8F000EAFC18B92068F6F70CFD08B1102013E443 +:10A900007168688C0978B0EBC10F01D313200BE498 +:10A910003946304601F02AF801467068082300786D +:10A92000C20005F1200007F0ECFED4E90012C0E9F6 +:10A9300000120020E3E710B50446032108F05AF89E +:10A940000146007800F00300022805D02046BDE84B +:10A95000104001F1140298E48A8A2046BDE81040B4 +:10A96000C7E470B50446032108F044F805460146E3 +:10A970002046FFF773FD002816D029462046FFF732 +:10A9800064FE002810D029462046FFF722FD00284B +:10A990000AD029462046FFF7CBFC002804D02946E0 +:10A9A0002046BDE87040A9E570BD2DE9F0410C4698 +:10A9B00080461EE0E178427811FB02F1CAB281695B +:10A9C00001230E3007F0D3FE077860681C22C1799E +:10A9D000491EC17107EB8701606802EBC10146188F +:10A9E0003946204600F0D5FF18B1304600F0E0FFB0 +:10A9F00020B16068C1790029DCD180E7FEF77CFDD9 +:10AA0000050000D1FFDF0A202872384600F0A6FFBB +:10AA100068813946204600F0B0FF0146606808238F +:10AA20004078C20006F1240007F0A1FED0E9001032 +:10AA3000C5E90310A5F80280284600F085FFB0782C +:10AA400000B9FFDFB078401EB07058E770B50C4613 +:10AA50000546032107F0CEFF01464068C279224433 +:10AA6000C2712846BDE870409FE72DE9FE4F82463F +:10AA7000507814460F464FF0000800284FD00128A8 +:10AA800007D0022822D0FFDF2068B8606068F86035 +:10AA900024E702AB0E2208215046FFF79CFB00285A +:10AAA000F2D00298152105230170217841700A2106 +:10AAB0004180C0F80480C0F80880A0F80C8062884B +:10AAC00082810E20CDE90008082221E0A6783046D8 +:10AAD00000F044FF054606EB86012C22786802EB65 +:10AAE000C1010822465A02AB11465046FFF773FBDC +:10AAF0000028C9D0029807210170217841700421F3 +:10AB0000418008218580C680CDE9001805230A46CA +:10AB100039465046FFF71FFB87F80880DEE6A67827 +:10AB2000022516B1022E13D0FFDF2A1D914602AB7B +:10AB300008215046FFF74FFB0028A5D002980121BD +:10AB4000022E0170217841704580868002D005E098 +:10AB50000625EAE7A188C180E1880181CDE9009856 +:10AB60000523082239465046D4E710B50446032190 +:10AB700007F040FF014600F108022046BDE8104002 +:10AB800073E72DE9F05F0C4601281DD0957992F806 +:10AB90000480567905EB85011F2202EBC10121F0EB +:10ABA000030B08EB060111FB05F14FF6FF7202EAF9 +:10ABB000C10909F1030115FB0611F94F21F0031A30 +:10ABC00040B101283DD124E06168E57891F800802A +:10ABD0004E78DFE75946786807F047FD606000B9B6 +:10ABE000FFDF594660681AF06AFDE57051467868E3 +:10ABF00007F03BFD6168486100B9FFDF60684269AA +:10AC000002EB09018161606880F80080606846702D +:10AC100017E0606852464169786807F051FD5A466E +:10AC20006168786807F04CFD032007F08BFE04464E +:10AC3000032007F08FFE201A012802D1786807F060 +:10AC400009FD0BEB0A00BDE8F09F0246002102203F +:10AC500097E773B5D24D0A202870009848B10024B8 +:10AC60004FEA0D0007F0E3FC002C01D10099696068 +:10AC70007CBD01240020F5E770B50C46154638214F +:10AC800020461AF01CFD012666700A2104F11C0002 +:10AC90001AF015FD05B9FFDF297A207861F301006C +:10ACA0002070A879002817D02A4621460020FFF7F7 +:10ACB00068FF6168402088706168C87061680871C9 +:10ACC0006168487161688871616828880881616875 +:10ACD000688848816068868170BDC878002802D085 +:10ACE000002201204DE7704770B50546002165F34D +:10ACF0001F4100200BF0A0FB0321284607F07AFE3D +:10AD0000040000D1FFDF21462846FFF767F900283D +:10AD100004D0207840F010002070012070BD2DE993 +:10AD2000FF4180460E460F0CFEF7E6FB050007D0FC +:10AD30006F800321384607F05DFE040008D106E06D +:10AD400004B03846BDE8F0411321F9F739BEFFDF02 +:10AD50005FEA080005D0B8F1060F18D0FFDFBDE8A4 +:10AD6000FF8120782A4620F0080020700020ADF8EE +:10AD7000020002208DF800004FF6FF70ADF80400CD +:10AD8000ADF8060069463846F9F711FAE7E7C6F369 +:10AD9000072101EB81021C23606803EBC202805C87 +:10ADA000042803D008280AD0FFDFD8E7012000904C +:10ADB0004FF440432A46204600F008FECFE704B097 +:10ADC0002A462046BDE8F041FFF7E7B82DE9F05FDD +:10ADD0000027B0F80A9090460C4605463E46B9F169 +:10ADE000400F01D2402001E0A9F140001FFA80FA93 +:10ADF000287AC01E08286BD2DFE800F00D04192065 +:10AE000058363C4772271026002C6CD0D5E9030138 +:10AE1000C4E902015CE070271226002C63D00A22EC +:10AE200005F10C0104F108001AF0EDFB50E0712768 +:10AE30000C26002C57D0E868A06049E07427102643 +:10AE40009CB3D5E90301C4E902016888032107F036 +:10AE5000D1FD8346FEF750FB02466888508051467C +:10AE60005846FFF751F833E075270A26ECB1A88958 +:10AE700020812DE076271426BCB105F10C0004F1E9 +:10AE8000080307C883E8070022E07727102664B18B +:10AE9000D5E90301C4E902016888032107F0AAFD8E +:10AEA00001466888FFF781FD12E01CE07327082641 +:10AEB000CCB16888032107F09DFD01460078C006EB +:10AEC00006D56888FFF78AF810B96888F8F786FD14 +:10AED000A8F800602CB12780A4F8069066806888E6 +:10AEE000A0800020AFE6A8F80060FAE72DE9FC4159 +:10AEF0000C461E4617468046032107F07BFD05469B +:10AF00000A2C0AD2DFE804F0050505050505090944 +:10AF10000907042303E0062301E0FFDF0023CDE956 +:10AF20000076224629464046FFF715F929E438B550 +:10AF30000546A0F57F40FF3830D0284607F08CFE4C +:10AF4000040000D1FFDF204607F011FA002815D0D9 +:10AF500001466A46204607F02CFA00980321B0F813 +:10AF60000540284607F046FD0546052C03D0402C39 +:10AF700005D2402404E0007A80B1002038BD403C76 +:10AF8000A4B2214600F005FD40B1686804EB8401DD +:10AF90003E2202EBC101405A0028EFD0012038BD0B +:10AFA0002C0000202DE9F04F054689B0408807F0BD +:10AFB00053FE040000D1FFDF06AA2046696800F0B6 +:10AFC000C1FC069C001F34F8031F21806388638046 +:10AFD000228881B28A4205D1042B0AD0052B1DD0CC +:10AFE000062B15D02A462046FFF7CDFB09B0BDE859 +:10AFF000F08F1646241D2A4621463046F7F73FFAC1 +:10B000000828F3D12A4621463046FCF7F4FBEDE749 +:10B010006888211D6B68FAF739FCE7E717466888EE +:10B02000032107F0E7FC4FF000088DF80480064686 +:10B03000ADF80680042FD9D36279002AD6D02079C2 +:10B040004FF6FF794FF01C0A13282CD008DC01289A +:10B0500078D0062847D0072875D0122874D106E08A +:10B06000142872D0152871D016286DD1ACE10C2FA0 +:10B070006AD1307800F00301022965D140F0080060 +:10B0800030706079B07001208DF804002089ADF82F +:10B0900008006089ADF80A00A089ADF80C00E089CD +:10B0A000ADF80E0019E0B07890429FD130780107DA +:10B0B0009CD5062F9AD120F0080030706888414650 +:10B0C00060F31F4100200BF0B7F902208DF8040057 +:10B0D000ADF808902089ADF80A0068882A4601A9D1 +:10B0E000F9F765F882E7082F80D12789B4F80A902C +:10B0F000402F01D2402001E0A7F1400080B28046FD +:10B100000146304600F045FC08B3716808EB880042 +:10B110002C2202EBC000095A4945E3D1FE4807AA98 +:10B12000D0E90210CDE9071060798DF81C0008F015 +:10B13000FF048DF81E4068883146FFF796FC2A46CA +:10B14000214639E0B6E014E03CE039E0E6E0F248C0 +:10B15000D0E90010CDE907106079ADF820708DF8C6 +:10B160001C00ADF82290688807AA3146FFF77DFCE5 +:10B170003CE7082FB6D16089B4F80880402801D296 +:10B18000402000E0403887B23946304600F001FCEC +:10B190000028A7D007EB870271680AEBC2000844B9 +:10B1A000028A42459ED1017808299BD14078617975 +:10B1B000884297D1F9B22A463046FEF7EEFE15E7EF +:10B1C0000E2F07D0CDF81C80CDF8208060798DF847 +:10B1D0001C00C8E76189E7898B46B4F80C903046BB +:10B1E000FEF73DFFABF14001402901D309204AE0C1 +:10B1F000B9F1170F01D3172F01D20B2043E04028DC +:10B200000ED000EB800271680AEBC200084401789E +:10B21000012903D1407861798842A9D00A2032E01F +:10B220003046FEF7FEFE014640282BD001EB81039D +:10B2300072680AEBC30002EB0008012288F80020C4 +:10B24000627988F80120706822894089B84200D963 +:10B250003846248A03232B72AA82EF812882A5F81C +:10B260000C906C82084600F079FB6881A8F8149075 +:10B27000A8F81870A8F80E40A8F810B0284600F0FA +:10B2800063FBB3E6042005212972A5F80A80E88152 +:10B2900001212973A049D1E90421CDE90721617970 +:10B2A0008DF81C10ADF81E00688807AA3146FFF71C +:10B2B000DCFBE3E7062FE4D3B078904215D1307879 +:10B2C000010712D520F0080030706888414660F30D +:10B2D0001F4100200BF0B0F802208DF804002089F7 +:10B2E000ADF80800ADF80A90F7E604213046FEF705 +:10B2F000CEFE04464028C4D00220830300902A4694 +:10B300002146304600F062FB4146688864F30F2115 +:10B3100060F31F4106200BF08FF867E60E2FB0D1C7 +:10B3200004213046FEF7B3FE81464028A9D04146AD +:10B33000688869F30F2160F31F4106200BF07CF849 +:10B34000208A0790E08900907068A7894089B842F8 +:10B3500000D938468346B4F80A80208905904846CB +:10B3600000F0FCFA6881079840B10220079B00902A +:10B370002A464946304600F029FB37E6B8F1170F58 +:10B380001ED3172F1CD30420287200986882EF81E7 +:10B39000A5F810B0A5F80C8009EB89020AEBC200F1 +:10B3A0007168009A0C180598A4F81480A4F818B0D5 +:10B3B000E2812082284600F0C7FA0620207015E6B8 +:10B3C00001200B230090D3E7082FA6D12189304616 +:10B3D000FEF745FE074640289FD007EB87027168BD +:10B3E0000AEBC2000844804600F0E9FA002894D134 +:10B3F0006489B8F80E002044B0F5803F05D3688812 +:10B400003A46314600F019FBF0E5002C85D0A8F84B +:10B410000E0068883A463146FFF7FDF8082028728A +:10B42000384600F09BFA6881AC8127E770B50D467D +:10B430000646032107F0DEFA040004D02078000756 +:10B4400004D5112070BD43F2020070BD2A4621468A +:10B450003046FEF71AFF18B9286860616868A06175 +:10B46000207840F008002070002070BD70B50D46B7 +:10B470000646032107F0BEFA040004D02078000736 +:10B4800004D4082070BD43F2020070BD2A46214654 +:10B490003046FEF72EFF00B9A582207820F0080084 +:10B4A0002070002070BD2DE9F04F0E4691B080460F +:10B4B000032107F09FFA0446404607F0DFFB0746EA +:10B4C0000020079008900990ADF830000A90029093 +:10B4D0000390049004B9FFDF0DF108091FBBFFDFE3 +:10B4E00021E038460BA9002206F004FE9DF82C004E +:10B4F00000F07F050A2D00D3FFDF6019017F491E90 +:10B5000001779DF82C0000060DD52A460CA907A846 +:10B51000FEF711FE02E00000AC5A020019F8051017 +:10B52000491C09F80510761EF6B2DAD204F134008F +:10B53000FA4D04F1260BDFF8E8A304F12A07069080 +:10B5400010E05846069900F06AFA064628700A2864 +:10B5500000D3FFDF5AF8261040468847E08CC05DD4 +:10B56000B04202D0208D0028EBD10A202870EC4D8B +:10B570004E4628350EE00CA907A800F050FA044604 +:10B58000375D55F8240000B9FFDF55F8242039460F +:10B5900040469047BDF81E000028ECD111B026E5CA +:10B5A00010B5032107F026FA040000D1FFDF0A21BD +:10B5B00004F11C001AF083F8207840F00400207099 +:10B5C00010BD10B50C46032107F014FA2044007F8B +:10B5D000002800D0012010BD2DE9F84F89461546FE +:10B5E0008246032107F006FA070004D02846F5F743 +:10B5F0006AFE40B903E043F20200BDE8F88F484616 +:10B60000F5F787FE08B11020F7E7786828B1698858 +:10B610000089814201D90920EFE7B9F800001C2414 +:10B6200018B1402809D2402008E03846FEF7F9FC5E +:10B630008046402819D11320DFE7403880B2804689 +:10B640000146384600F0A5F948B108EB8800796852 +:10B6500004EBC000085C012803D00820CDE70520DA +:10B66000CBE7FDF749FF06000BD008EB88007968AF +:10B6700004EBC0000C18B9F8000020B1E88910B143 +:10B6800013E01120B9E72888172802D36888172803 +:10B6900001D20720B1E7686838B12B1D2246414628 +:10B6A0003846FFF71DF90028A7D104F10C026946BE +:10B6B0002046FFF71BF8288860826888E082B9F886 +:10B6C000000030B102202070E889A080E889A0B194 +:10B6D0002BE003202070A889A08078688178402919 +:10B6E00005D180F8028039465046FEF721FE4046DB +:10B6F00000F034F9A9F8000021E07868218B408936 +:10B70000884200D908462083A6F802A0042030729F +:10B71000B9F800007081E0897082F181208B30825D +:10B72000A08AB081304600F00FF97868C1784029CE +:10B7300005D180F8038039465046FEF74AFE0020C6 +:10B740005BE770B50D460646032107F053F9040088 +:10B7500003D0402D04D2402503E043F2020070BD27 +:10B76000403DADB2294600F014F958B105EB850112 +:10B770001C22606802EBC101084400F020F918B1F6 +:10B78000082070BD052070BD2A462146304600F0D5 +:10B7900054F9002070BD2DE9F0410D461646804653 +:10B7A000032107F027F90446402D01D2402500E08F +:10B7B000403DADB28CB1294600F0EBF880B105EB0D +:10B7C00085011C22606802EBC1014718384600F071 +:10B7D000F6F838B10820BDE8F08143F20200FAE73C +:10B7E0000520F8E733463A4629462046FFF778F821 +:10B7F0000028F0D1EAB221464046FEF796FF00202D +:10B80000E9E72DE9F0410D4616468046032107F091 +:10B81000F1F80446402D01D2402500E0403DAFB292 +:10B8200024B13046F5F74FFD38B902E043F202008B +:10B83000D1E73068F5F747FD08B11020CBE739466E +:10B84000204600F0A6F860B107EB87011C22606873 +:10B8500002EBC1014518284600F0B1F818B10820E4 +:10B86000B9E70520B7E7B088A98A884201D90C203A +:10B87000B1E76168E88C4978B0EBC10F01D31320C0 +:10B88000A9E73946204600F078F8014660680823A9 +:10B890004078C20005F1240006F033FFD6E900121B +:10B8A000C0E90012FAB221464046FEF7B4FE00207D +:10B8B00091E72DE9F0470D461F469046814603214A +:10B8C00007F098F80446402D01D2402001E0A5F190 +:10B8D000400086B23CB14DB13846F5F738FD50B165 +:10B8E0001020BDE8F08743F20200FAE76068C8B1B3 +:10B8F000A0F80C8024E03146204600F04AF888B1D8 +:10B9000006EB86011C22606802EBC101451828463F +:10B9100000F055F840B10820E3E700002C000020BB +:10B92000C45A02000520DCE7A5F80880F2B22146DF +:10B930004846FEF7FAFE1FB1A88969890844388095 +:10B940000020CEE706F035BD017821F00F01491C3B +:10B9500021F0F00110310170FDF7D1BD10B50446A2 +:10B96000402800D9FFDF4034A0B210BD40684269D2 +:10B970000078484302EBC0007047C2784068037803 +:10B9800012FB03F24378406901FB032100EBC10085 +:10B990007047C2788A4209D9406801EB81011C22B4 +:10B9A00002EBC101405C08B10120704700207047E4 +:10B9B0000078062801D901207047002070470078E0 +:10B9C000062801D00120704700207047F0B401EB39 +:10B9D00081061C27446807EBC6063444049D0526EF +:10B9E0002670E3802571F0BCFEF78EBA10B5418950 +:10B9F00011B1FFF7DDFF08B1002010BD012010BD1F +:10BA000010B5C18C8278B1EBC20F04D9C18911B1D4 +:10BA1000FFF7CEFF08B1002010BD012010BD10B50A +:10BA20000C4601230A22011D06F0A1FE00782188A0 +:10BA3000012282409143218010BDF0B402EB8205C7 +:10BA40001C264C6806EBC505072363554B681C791B +:10BA5000402C03D11A71F0BCFEF700BDF0BC70475A +:10BA600010B5EFF3108000F0010472B6F948417888 +:10BA7000491C41704078012801D10AF01DF9002CC1 +:10BA800000D162B610BD70B5F24CA07848B901255E +:10BA9000A570FFF7E5FF0AF020F920B100200AF0B9 +:10BAA000EAF8002070BD4FF08040E570C0F8045304 +:10BAB000F7E770B5EFF3108000F0010572B6E54CC2 +:10BAC000607800B9FFDF6078401E6070607808B968 +:10BAD0000AF0F6F8002D00D162B670BDDD4810B551 +:10BAE000817821B10021C1708170FFF7E2FF002051 +:10BAF00010BD10B504460AF0F0F8D6498978084020 +:10BB000000D001202060002010BD10B5FFF7A8FF75 +:10BB10000AF0E3F802220123CE49540728B1CE48A7 +:10BB2000026023610320087202E00A72C4F8043341 +:10BB30000020887110BD2DE9F05FDFF8189342787E +:10BB4000817889F80420002689F80510074689F8CD +:10BB500006600078DFF804B3354620B1012811D023 +:10BB6000022811D0FFDF0AF0CAF84FF0804498B1E4 +:10BB70000AF0CCF8B0420FD130460AF0CBF80028DA +:10BB8000FAD041E00126EEE7FFF76AFF5846016868 +:10BB9000C907FCD00226E6E70120E060C4F80451A2 +:10BBA000AF490E600107D1F84412AD4AC1F34231EA +:10BBB00024321160AA49343108604FF0020AC4F8F7 +:10BBC00004A3A060A7480168C94341F3001101F133 +:10BBD0000108016841F01001016000E020BFD4F8C5 +:10BBE00004010028FAD030460AF094F80028FAD070 +:10BBF000B8F1000F04D19B48016821F010010160E9 +:10BC0000C4F808A3C4F8045199F805004E4688B159 +:10BC1000387878B90AF061F880460AF0F5F90146FB +:10BC20006FF00042B8F1000F02D0C6E9032101E035 +:10BC3000C6E90312DBF80000C00701D00AF049F89A +:10BC4000387810B13572BDE8F09F4FF01808C4F88D +:10BC50000883C4F82C510127C4F81870D4F82C01BB +:10BC60000028FBD0C4F80C51C4F810517948C01D0D +:10BC70000AF062F83570FFF748FF6761784930795C +:10BC800020310860C4F80483DDE770B5050000D1F9 +:10BC9000FFDF4FF080424FF0FF30C2F80803002171 +:10BCA000C2F80011C2F80411C2F80C11C2F8101148 +:10BCB000684C61700AF01DF810B10120A07060702E +:10BCC00066480068C00701D00AF003F82846BDE8BE +:10BCD000704030E75F48007A002800D001207047AC +:10BCE0002DE9FF5F6048D0F800805F4A5F49083265 +:10BCF00011608406D4F8080100B10120D4F82411A1 +:10BD000001B101218A46D4F81C1101B101218946F3 +:10BD1000D4F8201109B1012700E00027D4F8001160 +:10BD200001B101218B46D4F8041101B10121039125 +:10BD3000D4F80C1101B101210291D4F8101101B114 +:10BD40000121444D019129780026009120B1C4F8C9 +:10BD50000861012009F08FFFBAF1000F04D0C4F888 +:10BD60002461092009F087FFB9F1000F04D0C4F85D +:10BD70001C610A2009F07FFF27B1C4F820610B2065 +:10BD800009F079FF3348C01D09F0DEFF00B1FFDF85 +:10BD9000DFF8C4900127BBF1000F10D0C4F808737E +:10BDA000E87818B1EE70002009F065FF287A0228C3 +:10BDB00005D1032028720221C9F8001027610398D9 +:10BDC00008B1C4F80461029850B1C4F80C61287A33 +:10BDD000032800D0FFDFC9F800602F72FFF769FE6B +:10BDE000019838B1C4F81061287A012801D100F017 +:10BDF0005DF86761009838B12E70287A012801D16A +:10BE0000FFF783FEFFF755FE1248C01D09F0B2FF91 +:10BE10001549091DC1F80080BDE8FF9F0D4810B508 +:10BE2000C01D09F091FF0B4940B1012008704FF08F +:10BE3000E021C1F80002BDE8104011E6087A0128AF +:10BE400001D1FFF762FE0348BDE81040C01D09F0B4 +:10BE500091BF00003C000020340C00400C04004066 +:10BE60001805004010ED00E010050240010000013F +:10BE700070B5224CA07808B909F022FF012085078F +:10BE8000A861207A002603280AD100BFD5F80C014A +:10BE900020B9002009F03EFF0028F7D1C5F80C6159 +:10BEA00026724FF0FF30C5F8080370BD70B5134C13 +:10BEB0006079F0B1012803D0A179401E814218DADF +:10BEC00009F00BFF05460AF09FF86179012902D9B4 +:10BED000A179491CA1710DB1216900E0E168411A05 +:10BEE000022902DA11F1020F06DC0DB1206100E037 +:10BEF000E060BDE8704008E670BD00003C00002036 +:10BF000010B5202000F07FF8202000F08DF84D497A +:10BF1000202081F80004F5F771FA4B4908604B487E +:10BF2000D0F8041341F00101C0F80413D0F8041351 +:10BF300041F08071C0F80413424901201C39C1F856 +:10BF4000000110BD10B5202000F05DF83E48002132 +:10BF5000C8380160001D01603D4A481E10603B4A20 +:10BF6000C2F80803384B1960C2F80001C2F860013A +:10BF700038490860BDE81040202000F055B8344929 +:10BF80003548091F086070473149334808607047D9 +:10BF90002D48C8380160001D521E026070472C49B0 +:10BFA00001200860BFF34F8F70472DE9F041284909 +:10BFB000D0F8188028480860244CD4F800010025E7 +:10BFC000244E6F1E28B14046F5F776F940B900219E +:10BFD00011E0D4F8600198B14046F5F76DF948B129 +:10BFE000C4F80051C4F860513760BDE8F04120202A +:10BFF00000F01AB831684046BDE8F04119F08ABB3C +:10C00000FFDFBDE8F08100280DDB00F01F020121F9 +:10C0100091404009800000F1E020C0F88011BFF39A +:10C020004F8FBFF36F8F7047002809DB00F01F02AE +:10C03000012191404009800000F1E020C0F8801209 +:10C040007047000020E000E0C80602400000024007 +:10C050001805024000040240010000010F4A126866 +:10C060000D498A420CD118470C4A12680A4B9A4271 +:10C0700006D101B509F09AFFFFF781FFBDE8014045 +:10C08000074909680958084706480749054A064B01 +:10C090007047000000000000BEBAFECA5400002035 +:10C0A000040000208013002080130020F8B51D46F6 +:10C0B000DDE906470E000AD006F0E0FD2346FF1D2D +:10C0C000BCB231462A46009406F0EDF9F8BDD0190D +:10C0D0002246194619F052FA2046F8BD70B50D46B1 +:10C0E0000446102119F0C9FA258117206081A07B30 +:10C0F00040F00A00A07370BD4FF6FF720A8001463F +:10C1000002200AF099B9704700897047827BD307F3 +:10C1100001D1920703D48089088000207047052050 +:10C120007047827B920700D58181704701460020CD +:10C13000098841F6FE52114200D00120704700B537 +:10C140000346807BC00701D0052000BD59811846F9 +:10C15000FFF7ECFFC00703D0987B40F00400987312 +:10C16000987B40F001009873002000BD827B52074D +:10C1700000D509B14089704717207047827B61F371 +:10C18000C302827370472DE9FC5F0E4604460178B6 +:10C190009646012000FA01F14DF6FF5201EA02092C +:10C1A00062684FF6FF7B1188594502D10920BDE82E +:10C1B000FC9FB9F1000F05D041F6FE55294201D090 +:10C1C0000120F4E741EA090111801D0014D0002389 +:10C1D0002B7094F800C0052103221F464FF0020A7D +:10C1E000BCF10E0F76D2DFE80CF0F909252F476479 +:10C1F0006B77479193B4D1D80420D8E76168208940 +:10C200008B7B9B0767D517284AD30B89834247D37B +:10C210008989172901D3814242D185F800A0A5F868 +:10C2200001003280616888816068817B21F00201B1 +:10C230008173C6E0042028702089A5F80100608978 +:10C24000A5F803003180BCE0208A3188C01D1FFAA8 +:10C2500080F8414524D3062028702089A5F80100E4 +:10C260006089A5F80300A089A5F805000721208AA8 +:10C27000CDE90001636941E00CF0FF00082810D00F +:10C28000082028702089A5F801006089A5F803001E +:10C2900031806A1D694604F10C0008F057F910B1AD +:10C2A0005EE01020EDE730889DF8001008443080F3 +:10C2B00087E00A2028702089A5F80100328044E038 +:10C2C0000C2028702089A5F801006089A5F80300DA +:10C2D00031803AE082E064E02189338800EB41025A +:10C2E0001FFA82F843453BD3B8F1050F38D30E222D +:10C2F0002A700BEA4101CDE90010E36860882A4604 +:10C300007146FFF7D3FEA6F800805AE0402028705F +:10C3100060893188C01C1FFA80F8414520D32878F5 +:10C32000714620F03F00123028702089A5F80100E6 +:10C330006089CDE9000260882A46E368FFF7B6FE0F +:10C34000A6F80080287840063BD461682089888060 +:10C3500037E0A0893288401D1FFA80F8424501D29B +:10C3600004273DE0162028702089A5F80100608987 +:10C37000A5F80300A089CDE9000160882A4671462E +:10C380002369FFF793FEA6F80080DEE718202870E7 +:10C39000207A6870A6F800A013E061680A88920409 +:10C3A00001D405271CE0C9882289914201D00627C3 +:10C3B00016E01E21297030806068018821F4005148 +:10C3C0000180B9F1000F0BD0618878230022022090 +:10C3D00009F088FF61682078887006E033800327C1 +:10C3E0006068018821EA090101803846DFE62DE90D +:10C3F000FF4F85B01746129C0D001E461CD03078AA +:10C40000C10703D000F03F00192801D9012100E045 +:10C4100000212046FFF7AAFEA8420DD32088A0F5F0 +:10C420007F41FF3908D03078410601D4000605D598 +:10C43000082009B0BDE8F08F0720FAE700208DF84A +:10C4400000008DF8010030786B1E00F03F0C0121D8 +:10C45000A81E4FF0050A4FF002094FF0030B9AB2E5 +:10C46000BCF1200F75D2DFE80CF08B10745E74689D +:10C47000748C749C74B674BB74C974D574E274748F +:10C4800074F274F074EF74EE748B052D78D18DF81E +:10C490000090A0788DF804007088ADF8060030791F +:10C4A0008DF80100707800F03F000C2829D00ADCDC +:10C4B000A0F10200092863D2DFE800F012621562E1 +:10C4C0001A621D622000122824D004DC0E281BD022 +:10C4D0001028DBD11BE016281FD01828D6D11FE06A +:10C4E0002078800701E020784007002848DAEFE054 +:10C4F00020780007F9E72078C006F6E72078800664 +:10C50000F3E720784006F0E720780006EDE7208882 +:10C51000C005EAE720884005E7E720880005E4E752 +:10C520002088C004E1E72078800729D5032D27D192 +:10C530008DF800B0B6F8010082E0217849071FD5D8 +:10C54000062D1DD381B27078012803D0022817D19F +:10C5500002E0CAE0022000E0102004228DF8002052 +:10C5600072788DF80420801CB1FBF0F2ADF8062043 +:10C5700092B242438A4203D10397ADF80890A7E0F4 +:10C580007AE02078000777D598B282088DF800A06D +:10C59000ADF80420B0EB820F6ED10297ADF8061013 +:10C5A00096E02178C90667D5022D65D381B20620B1 +:10C5B0008DF80000707802285ED300BFB1FBF0F266 +:10C5C0008DF80400ADF8062092B242438A4253D15E +:10C5D000ADF808907BE0207880064DD5072003E079 +:10C5E000207840067FD508208DF80000A088ADF89F +:10C5F0000400ADF80620ADF8081068E020780006C9 +:10C6000071D50920ADF804208DF80000ADF80610B2 +:10C6100002975DE02188C90565D5022D63D381B2FB +:10C620000A208DF80000707804285CD3C6E72088C3 +:10C63000400558D5012D56D10B208DF80000A0885B +:10C64000ADF8040044E021E026E016E0FFE7208892 +:10C65000000548D5052D46D30C208DF80000A08894 +:10C66000ADF80400B6F803006D1FADF80850ADF842 +:10C670000600ADF80AA02AE035E02088C00432D5D3 +:10C68000012D30D10D208DF8000021E0208880049C +:10C6900029D4B6F80100E080A07B000723D5032D44 +:10C6A00021D3307800F03F001B2818D00F208DF8E0 +:10C6B0000000208840F40050A4F80000B6F8010003 +:10C6C000ADF80400ED1EADF80650ADF808B00397C4 +:10C6D00069460598F5F71EFC050008D016E00E2007 +:10C6E0008DF80000EAE7072510E008250EE0307815 +:10C6F00000F03F001B2809D01D2807D00220059913 +:10C7000009F09AFE208800F400502080A07B4007AA +:10C7100008D52046FFF70AFDC00703D1A07B20F013 +:10C720000400A073284684E61FB5022806D1012024 +:10C730008DF8000088B26946F5F7ECFB1FBD0000DC +:10C74000F8B51D46DDE906470E000AD006F096FA58 +:10C750002346FF1DBCB231462A46009405F0A3FED5 +:10C76000F8BDD0192246194618F008FF2046F8BD3A +:10C770002DE9FF4F8DB09B46DDE91B57DDF87CA00E +:10C780000C46082B05D0E06901F002F950B11020E9 +:10C79000D2E02888092140F0100028808AF8001093 +:10C7A000022617E0E16901208871E2694FF4205107 +:10C7B0009180E1698872E06942F601010181E069D6 +:10C7C000002181732888112140F0200028808AF8F8 +:10C7D0000010042638780A900A2038704FF00209B9 +:10C7E00004F118004D460C9001F095FBB04681E035 +:10C7F000BBF1100F0ED1022D0CD0A9EB0800801C4C +:10C8000080B20221CDE9001005AB52461E990D9869 +:10C81000FFF796FFBDF816101A98814203D9F74822 +:10C8200000790F9004E003D10A9808B138702FE026 +:10C830004FF00201CDE900190DF1160352461E9981 +:10C840000D98FFF77DFF1D980088401B801B83B269 +:10C85000C6F1FF00984200D203461E990BA8D9B139 +:10C860005FF00002DDF878C0CDE9032009EB060196 +:10C8700089B2CDE901C10F980090BDF816100022D1 +:10C880000D9801F0CBFB387070B1C0B2832807D08F +:10C89000BDF8160020833AE00AEB09018A19E1E7A6 +:10C8A000022011B0BDE8F08FBDF82C00811901F015 +:10C8B000FF08022D0DD09AF80120424506D1BDF89F +:10C8C0002010814207D0B8F1FF0F04D09AF8018000 +:10C8D0001FE08AF80180C94800680178052902D163 +:10C8E000BDF81610818009EB08001FFA80F905EBEE +:10C8F000080085B2DDE90C1005AB0F9A01F00EFBC4 +:10C9000028B91D980088411B4145BFF671AF022D23 +:10C9100013D0BBF1100F0CD1A9EB0800801C81B221 +:10C920000220CDE9000105AB52461E990D98FFF794 +:10C9300007FF1D980580002038700020B1E72DE921 +:10C94000F8439C46089E13460027B26B9AB3491FD2 +:10C950008CB2F18FA1F57F45FF3D05D05518AD880C +:10C960002944891D8DB200E000252919B6F83C80C4 +:10C970000831414520D82A44BCF8011022F8021B96 +:10C98000BCF8031022F8021B984622F8024B91468D +:10C9900006F062F94FF00C0C41464A462346CDF8AA +:10C9A00000C005F04CFDF587B16B00202944A41DA3 +:10C9B0002144088003E001E0092700E0832738468E +:10C9C000BDE8F88310B50B88848F9C420CD9846B2A +:10C9D000E018048844B1848824F40044A41D23444E +:10C9E0000B801060002010BD0A2010BD2DE9F0471B +:10C9F0008AB00025904689468246ADF81850072730 +:10CA00004BE0059806888088000446D4A8F80060AA +:10CA100007A8019500970295CDE903504FF40073E4 +:10CA200000223146504601F0F9FA04003CD1BDF82D +:10CA30001800ADF82000059804888188B44216D10A +:10CA40000A0414D401950295039521F4004100973E +:10CA5000049541F4804342882146504601F0B4F8E1 +:10CA600004000BD10598818841F40041818005AA1A +:10CA700008A94846FFF7A6FF0400DCD000970598F8 +:10CA800002950195039504950188BDF81C3000229C +:10CA9000504601F099F80A2C06D105AA06A9484685 +:10CAA000FFF790FF0400ACD0ADF8185004E00598F3 +:10CAB000818821F40041818005AA06A94846FFF734 +:10CAC00081FF0028F3D00A2C03D020460AB0BDE82D +:10CAD000F0870020FAE710B50C46896B86B051B19B +:10CAE0000C218DF80010A18FADF80810A16B0191F9 +:10CAF0006946FAF718FB00204FF6FF71A063E18743 +:10CB0000A08706B010BD2DE9F0410D460746896BA0 +:10CB10000020069E1446002911D0012B0FD1324669 +:10CB200029463846FFF762FF002808D1002C06D0BE +:10CB3000324629463846BDE8F04100F038BFBDE82E +:10CB4000F0812DE9FC411446DDE9087C0E46DDE963 +:10CB50000A15521DBCF800E092B2964502D2072099 +:10CB6000BDE8FC81ACF8002017222A70A5F801600E +:10CB7000A5F803300522CDE900423B462A46FFF7DF +:10CB8000DFFD0020ECE770B50C4615464821204635 +:10CB900018F095FD04F1080044F81C0F00204FF632 +:10CBA000FF71E06161842084A5841720E08494F8FB +:10CBB0002A0040F00A0084F82A0070BD4FF6FF7288 +:10CBC0000A800146032009F037BC30B585B00C4619 +:10CBD0000546FFF780FFA18E284629B101218DF877 +:10CBE00000106946FAF79FFA0020E0622063606354 +:10CBF00005B030BDB0F8400070470000580000207C +:10CC000090F84620920703D4408808800020F3E77C +:10CC10000620F1E790F846209207EDD5A0F84410E1 +:10CC2000EAE70146002009880A0700D5012011F033 +:10CC3000F00F01D040F00200CA0501D540F0040019 +:10CC40008A0501D540F008004A0501D540F01000E2 +:10CC50000905D1D540F02000CEE700B5034690F895 +:10CC60004600C00701D0062000BDA3F842101846B8 +:10CC7000FFF7D7FF10F03E0F05D093F8460040F0C5 +:10CC8000040083F8460013F8460F40F001001870C6 +:10CC9000002000BD90F84620520700D511B1B0F831 +:10CCA0004200A9E71720A7E710F8462F61F3C30257 +:10CCB0000270A1E72DE9FF4F9BB00E00DDE92B3498 +:10CCC000DDE92978289D24D02878C10703D000F019 +:10CCD0003F00192801D9012100E000212046FFF77B +:10CCE000D9FFB04215D32878410600F03F010CD49B +:10CCF0001E290CD0218811F47F6F0AD13A8842B1E5 +:10CD0000A1F57F42FF3A04D001E0122901D10006CB +:10CD100002D504201FB0C5E5F9491D984FF0000A5F +:10CD200008718DF818A08DF83CA00FAA0A60ADF824 +:10CD30001CA0ADF850A02978994601F03F02701F61 +:10CD40005B1C04F1180C4FF0060E4FF0040BCDF8ED +:10CD500058C01F2A7ED2DFE802F07D7D107D267D3F +:10CD6000AC7DF47DF37DF27DF17DF47DF07D7D7D04 +:10CD7000EF7DEE7D7D7D7D7DED0094F84610B5F86C +:10CD80000100890701D5032E02D08DF818B022E3E7 +:10CD90004FF40061ADF85010608003218DF83C1015 +:10CDA000ADF84000D8E2052EEFD1B5F801002083A0 +:10CDB000ADF81C00B5F80310618308B1884201D9B1 +:10CDC00001207FE10020A07220814FF6FF702084B7 +:10CDD000169801F0A0F8052089F8000002200290C2 +:10CDE00083460AAB1D9A16991B9801F097F890BBE1 +:10CDF0009DF82E00012804D0022089F8010010209F +:10CE000003E0012089F8010002200590002203A917 +:10CE10000BA807F09BFBE8BB9DF80C00059981422D +:10CE20003DD13A88801CA2EB0B01814237DB02998D +:10CE30000220CDE900010DF12A034A4641461B9824 +:10CE4000FFF77EFC02980BF1020B801C80B217AA40 +:10CE500003A901E0A0E228E002900BA807F076FB0E +:10CE600002999DF80C00CDE9000117AB4A464146F6 +:10CE70001B98FFF765FC9DF80C100AAB0BEB01004B +:10CE80001FFA80FB02981D9A084480B202901699FE +:10CE90001B9800E003E001F041F80028B6D0BBF198 +:10CEA000020F02D0A7F800B053E20A208DF8180054 +:10CEB0004FE200210391072EFFF467AFB5F80100A0 +:10CEC0002083ADF81C00B5F80320628300283FF4EE +:10CED00077AF90423FF674AF0120A072B5F805001D +:10CEE00020810020A073E06900F052FD78B9E1696B +:10CEF00001208871E2694FF420519180E1698872C4 +:10CF0000E06942F601010181E06900218173F01FAF +:10CF100020841E98606207206084169800F0FBFF52 +:10CF2000072089F800000120049002900020ADF84D +:10CF30002A0028E01DE2A3E13AE1EAE016E2AEE0D1 +:10CF400086E049E00298012814D0E0698079012840 +:10CF500003D1BDF82800ADF80E00049803ABCDE96D +:10CF600000B04A4641461B98FFF7EAFB0498001DB3 +:10CF700080B20490BDF82A00ADF80C00ADF80E00A8 +:10CF8000059880B202900AAB1D9A16991B9800F082 +:10CF9000C5FF28B902983988001D05908142D1D279 +:10CFA0000298012881D0E0698079012805D0BDF878 +:10CFB0002810A1F57F40FF3803D1BDF82800ADF857 +:10CFC0000E00049803ABCDE900B04A4641461B98D9 +:10CFD000FFF7B6FB0298BBE1072E02D0152E7FF4B7 +:10CFE000D4AEB5F801102183ADF81C10B5F80320BC +:10CFF000628300293FF4E4AE91423FF6E1AE0121A5 +:10D00000A1724FF0000BA4F808B084F80EB0052E02 +:10D0100007D0C0B2691DE26907F079FA00287FF4F1 +:10D0200044AF4FF6FF70208401A906AA14A8CDF8DA +:10D0300000B081E885032878214600F03F031D9A5F +:10D040001B98FFF795FB8246208BADF81C0080E112 +:10D050000120032EC3D14021ADF85010B5F80110C6 +:10D060002183ADF81C100AAAB8F1000F00D00023EC +:10D07000CDE9020304921D98CDF804800090388811 +:10D080000022401E83B21B9800F0C8FF8DF81800E4 +:10D0900090BB0B2089F80000BDF8280037E04FF066 +:10D0A000010C052E9BD18020ADF85000B5F8011081 +:10D0B0002183B5F803002084ADF81C10B0F5007F83 +:10D0C00003D907208DF8180085E140F47C422284C2 +:10D0D0000CA8B8F1000F00D00023CDE90330CDE952 +:10D0E000018C1D9800903888401E83B21B9800F078 +:10D0F00095FF8DF8180028B18328A8D10220BDE043 +:10D10000580000200D2189F80010BDF83000401CA7 +:10D110001EE1032E04D248067FF537AE002017E14A +:10D12000B5F80110ADF81C102878400602D58DF82E +:10D130003CE002E007208DF83C004FF0000803209F +:10D14000CDE902081E9BCDF810801D980193A6F131 +:10D15000030B00901FFA8BF342461B9800F034FD3E +:10D160008DF818008DF83C80297849060DD5208867 +:10D17000C00506D5208BBDF81C10884201D1C4F82B +:10D18000248040468DF81880E2E0832801D14FF0DA +:10D19000020A4FF48070ADF85000BDF81C002083E7 +:10D1A000A4F820B01E986062032060841321CCE0B4 +:10D1B000052EFFF4EAADB5F80110ADF81C10A28FF2 +:10D1C00062B3A2F57F43FE3B28D008228DF83C20B5 +:10D1D0004FF0000B0523CDE9023BDDF878C0CDF818 +:10D1E00010B01D9A80B2CDF804C040F40043009204 +:10D1F000B5F803201B9800F0E7FC8DF83CB04FF425 +:10D2000000718DF81800ADF85010832810D0F8B1D7 +:10D21000A18FA1F57F40FE3807D0DCE00B228DF80E +:10D220003C204FF6FE72A287D2E7A4F83CB0D2E0D1 +:10D2300000942B4631461E9A1B98FFF780FB8DF811 +:10D24000180008B183284BD1BDF81C00208355E796 +:10D2500000942B4631461E9A1B98FFF770FB8DF801 +:10D260001800E8BBE18FA06B0844811D8DE88203A4 +:10D270004388828801881B98FFF763FC824668E038 +:10D2800095F80180022E70D15FEA080002D0B8F153 +:10D29000010F6AD109208DF83C0007A800908DF895 +:10D2A00040804346002221461B98FFF72CFC8DF856 +:10D2B00042004FF0000B8DF843B050B9B8F1010FA8 +:10D2C00012D0B8F1000F04D1A18FA1F57F40FF3833 +:10D2D0000AD0A08F40B18DF83CB04FF4806000E0E0 +:10D2E00037E0ADF850000DE00FA91B98F9F71BFFD0 +:10D2F00082468DF83CB04FF48060ADF85000BAF132 +:10D30000020F06D0FC480068C07928B18DF81800DB +:10D3100027E0A4F8188044E0BAF1000F03D0812080 +:10D320008DF818003DE007A80090434601222146F1 +:10D330001B98FFF7E8FB8DF8180021461B98FFF7B4 +:10D34000CAFB9DF8180020B9192189F800100120A6 +:10D3500038809DF83C0020B10FA91B98F9F7E3FE37 +:10D360008246BAF1000F33D01BE018E08DF818E0C8 +:10D3700031E02078000712D5012E10D10A208DF857 +:10D380003C00E088ADF8400003201B9909F054F8F8 +:10D390000820ADF85000C1E648067FF5F6AC4FF026 +:10D3A000040A2088BDF8501008432080BDF85000C2 +:10D3B00080050BD5A18FA1F57F40FE3806D11E98C0 +:10D3C000E06228982063A6864FF0030A5046A1E445 +:10D3D0009DF8180078B1012089F80000297889F8B3 +:10D3E0000110BDF81C10A9F802109DF8181089F85A +:10D3F0000410052038802088BDF850108843208014 +:10D40000E4E72DE9FF4F8846087895B00121814077 +:10D410004FF20900249C0140ADF820102088DDF86F +:10D420008890A0F57F424FF0000AFF3A06D039B14C +:10D43000000705D5012019B0BDE8F08F0820FAE7F4 +:10D44000239E4FF0000B0EA886F800B018995D4699 +:10D450000988ADF83410A8498DF81CB0179A0A71E4 +:10D460008DF838B0086098F8000001283BD00228F9 +:10D4700009D003286FD1307820F03F001D30307084 +:10D48000B8F80400E08098F800100320022904D1C5 +:10D49000317821F03F011B31317094F846100907B3 +:10D4A00059D505ABB9F1000F13D0002102AA82E8CB +:10D4B0000B000720CDE90009BDF83400B8F80410CE +:10D4C000C01E83B20022159800F0A8FD0028D1D11B +:10D4D00001E0F11CEAE7B8F80400A6F80100BDF885 +:10D4E0001400C01C04E198F805108DF81C1098F881 +:10D4F0000400012806D04FF4007A02282CD003281B +:10D50000B8D16CE12188B8F8080011F40061ADF8D9 +:10D51000201020D017281CD3B4F84010814218D313 +:10D52000B4F84410172901D3814212D1317821F087 +:10D530003F01C91C3170A6F801000321ADF8341079 +:10D54000A4F8440094F8460020F0020084F8460055 +:10D5500065E105257EE177E1208808F1080700F400 +:10D56000FE60ADF8200010F0F00F1BD010F0C00FDF +:10D5700003D03888228B9042EBD199B9B878C00794 +:10D5800010D0B9680720CDE902B1CDF804B0009001 +:10D59000CDF810B0FB88BA883988159800F014FBD4 +:10D5A0000028D6D12398BDF82010401C80294ED0E9 +:10D5B00006DC10290DD020290BD0402987D124E08A +:10D5C000B1F5807F6ED051457ED0B1F5806F97D197 +:10D5D000DEE0C80601D5082000E0102082460DA933 +:10D5E00007AA0520CDE902218DF83800ADF83CB03E +:10D5F000CDE9049608A93888CDE9000153460722F1 +:10D6000021461598FFF7B4F8A8E09DF81C200121E9 +:10D610004FF00A0A002A9BD105ABB9F1000F00D0E8 +:10D620000020CDE902100720CDE90009BDF8340043 +:10D630000493401E83B2218B0022159800F0EEFC6B +:10D640008DF81C000B203070BDF8140020E09DF810 +:10D650001C2001214FF00C0A002A22D113ABB9F192 +:10D66000000F00D00020CDE902100720CDE900090D +:10D670000493BDF83400228C401E83B2218B159890 +:10D6800000F0CCFC8DF81C000D203070BDF84C0073 +:10D69000401CADF8340005208DF83800208BADF823 +:10D6A0003C00BCE03888218B88427FF452AF9DF863 +:10D6B0001C004FF0120A00281CD1606AA8B1B8788B +:10D6C000C0073FF446AF00E018E0BA680720CDE994 +:10D6D00002B2CDF804B00090CDF810B0FB88BA8843 +:10D6E000159800F071FA8DF81C001320307001209D +:10D6F000ADF8340093E00000580000203988208BFA +:10D700008142D2D19DF81C004FF0160A0028A06B70 +:10D7100008D0E0B34FF6FF7000215F46ADF808B0C7 +:10D72000019027E068B1B978C907BED1E18F0DAB90 +:10D730000844821D03968DE80C0243888288018884 +:10D7400009E0B878C007BCD0BA680DAB03968DE885 +:10D750000C02BB88FA881598FFF7F3F905005ED034 +:10D76000072D72D076E0019005AA02A92046FFF7A6 +:10D7700029F90146E28FBDF80800824201D0002954 +:10D78000F1D0E08FA16B084407800198E08746E064 +:10D790009DF81C004FF0180A40B1208BC8B13888A2 +:10D7A000208321461598FFF796F938E004F1180018 +:10D7B0000090237E012221461598FFF7A4F98DF8E9 +:10D7C0001C000028EDD1192030700120ADF8340084 +:10D7D000E7E7052521461598FFF77DF93AE020880F +:10D7E00000F40070ADF8200050452DD1A08FA0F5B9 +:10D7F0007F41FE3901D006252CE0D8F808004FF013 +:10D80000160A48B1A063B8F80C10A1874FF6FF7153 +:10D81000E187A0F800B002E04FF6FF70A087BDF8E6 +:10D82000200030F47F611AD078230022032015995C +:10D8300008F058FD98F8000020712088BDF82010ED +:10D84000084320800EE000E007252088BDF8201066 +:10D8500088432080208810F47F6F1CD03AE0218814 +:10D86000814321809DF8380020B10EA91598F9F761 +:10D870005AFC05469DF81C000028EBD086F801A054 +:10D8800001203070208B70809DF81C0030710520C5 +:10D89000ADF83400DEE7A18EE1B118980DAB008839 +:10D8A000ADF834002398CDE90304CDE90139206BAC +:10D8B0000090E36A179A1598FFF7FCF905460120D6 +:10D8C0008DF838000EA91598F9F72DFC00B1054622 +:10D8D000A4F834B094F8460040070AD52046FFF774 +:10D8E000A0F910F03E0F04D114F8460F20F0040008 +:10D8F00020701898BDF83410018028469BE500B5CB +:10D9000085B0032806D102208DF8000088B2694650 +:10D91000F9F709FC05B000BD10B5384C0B7822684A +:10D92000012B02D0022B2AD111E013780BB1052B69 +:10D9300001D10423137023688A889A802268CB88D7 +:10D94000D38022680B891381498951810DE08B882E +:10D9500093802268CB88D38022680B8913814B89FE +:10D9600053818B899381096911612168F9F7DBFB88 +:10D97000226800210228117003D0002800D08120E5 +:10D9800010BD832010BD806B002800D0012070479F +:10D990008178012909D10088B0F5205F03D042F6D3 +:10D9A0000101884201D10020704707207047F0B57F +:10D9B00087B0002415460E460746ADF8184011E022 +:10D9C00005980088288005980194811DCDE90241C1 +:10D9D000072104940091838842880188384600F02A +:10D9E000F3F830B905AA06A93046FEF7EBFF002888 +:10D9F000E6D00A2800D1002007B0F0BD5800002072 +:10DA000010B58B7883B102789A4205D10B885BB14F +:10DA100002E08B79091D4BB18B789A42F9D1B0F8AD +:10DA200001300C88A342F4D1002010BD812010BD2C +:10DA3000072826D012B1012A27D103E0497801F046 +:10DA4000070102E04978C1F3C20105291DD2DFE8D0 +:10DA500001F00318080C12000AB1032070470220DD +:10DA6000704704280DD250B10DE0052809D2801E60 +:10DA7000022808D303E0062803D0032803D005209A +:10DA80007047002070470F20704781207047C0B258 +:10DA900082060BD4000607D5FE48807A4143C01D9C +:10DAA00001EBD00080B270470846704700207047F5 +:10DAB00070B513880B800B781C0625D5F54CA47A1D +:10DAC000844204D843F010000870002070BD9568AF +:10DAD00000F0070605EBD0052D78F54065F304133B +:10DAE0000B701378D17803F0030341EA032140F26D +:10DAF0000123B1FBF3F503FB15119268E41D00FB54 +:10DB0000012000EBD40070BD906870BD37B514469D +:10DB1000BDF8041011809DF804100A061ED5C1F34B +:10DB20000013DC49A568897A814208D8FE2811D102 +:10DB3000C91DC9085A42284617F097FD0AE005EBAF +:10DB4000D00100F00702012508789540A8439340D2 +:10DB500018430870207820F0100020703EBD2DE999 +:10DB6000F0410746C81C0E4620F00300B04202D028 +:10DB70008620BDE8F081C74D002034462E60AF807E +:10DB80002881AA72E8801AE0E988491CE9808106A8 +:10DB900014D4E17800F0030041EA002040F20121B2 +:10DBA000B0FBF1F201FB12012068FFF770FF298939 +:10DBB000084480B22881381A3044A0600C342078A0 +:10DBC0004107E1D40020D4E72DE9FF4F89B0164684 +:10DBD000DDE9168A0F46994623F44045084600F0D1 +:10DBE0000DFB04000FD0099804F0CAFE02902078C3 +:10DBF00000060AD5A748817A0298814205D8872075 +:10DC00000DB0BDE8F08F0120FAE7224601A9029885 +:10DC1000FFF74EFF834600208DF80C004046B8F118 +:10DC2000070F1AD001222146FFF702FF0028E7D193 +:10DC30002078400611D502208DF80C00ADF8107048 +:10DC4000BDF80400ADF81200ADF814601898ADF8F6 +:10DC50001650CDF81CA0ADF818005FEA094004D5B5 +:10DC600000252E46A84601270CE02178E07801F037 +:10DC7000030140EA012040F20121B0FBF1F28046AD +:10DC800001FB12875FEA494009D5B84507D1A17861 +:10DC9000207901F0030140EA0120B04201D3BE42E5 +:10DCA00001D90720ACE7A8191FFA80F9B94501D9B5 +:10DCB0000D20A5E79DF80C0028B103A90998F9F7F4 +:10DCC00030FA00289CD1B84507D1A0784FEA192135 +:10DCD00061F30100A07084F804901A9800B10580E7 +:10DCE000199850EA0A0027D0199830B10BEB0600BA +:10DCF0002A46199917F042FC0EE00BEB060857462E +:10DD0000189E099804F0A8FF2B46F61DB5B23946B7 +:10DD10004246009504F093FB224601A90298FFF7C2 +:10DD2000C7FE9DF80400224620F010008DF8040084 +:10DD3000DDE90110FFF7EAFE002061E72DE9FF4F62 +:10DD4000DFF8509182461746B9F80610D9F800005E +:10DD500001EB410100EB810440F20120B2FBF0F144 +:10DD600085B000FB11764D46DDF84C8031460698B3 +:10DD7000FFF78DFE29682A898B46611A0C31014410 +:10DD80001144AB8889B28B4202D8842009B038E7AD +:10DD90000699CDB2290603D5A90601D50620F5E7D7 +:10DDA000B9F806C00CF1010C1FFA8CFCA9F806C0EA +:10DDB000149909B1A1F800C0A90602D5C4F80880D9 +:10DDC00007E0104480B2A9F80800191A01EB0B0013 +:10DDD000A0602246FE200699FFF798FEE7702671A4 +:10DDE0002078390A61F30100320AA17840F004007A +:10DDF00062F30101A17020709AF802006071BAF814 +:10DE00000000E08000262673280602D599F80A70E3 +:10DE100000E00127A80601D54FF000084D46002478 +:10DE20004FF007090FE0CDE902680196CDF80090A8 +:10DE30000496E9882046129B089AFFF7C5FE002841 +:10DE4000A4D1641CE4B2BC42EDD300209EE72DE9CE +:10DE5000F047804600F0D2F9070005D0002644467E +:10DE60000C4D40F2012919E00120BDE8F087204661 +:10DE700000F0C4F90278C17802F0030241EA0222FC +:10DE8000B2FBF9F309FB13210068FFF700FE3044F1 +:10DE900086B201E0F8050020641CA4B2E988601E87 +:10DEA0008142E4DCA8F10100E8802889801B2881F8 +:10DEB00000203870D9E710B5144631B1491E2180D1 +:10DEC00004F05EFDA070002010BD012010BD10B553 +:10DED000D24904460088CA88904201D30A2010BD66 +:10DEE000096800EB400001EB80025079A072D088F5 +:10DEF00020819178107901F0030140EA0120A0818E +:10DF0000A078E11CFFF7D4FD20612088401C208010 +:10DF1000E080002010BD0121018270472DE9FF4FF4 +:10DF200085B04FF6FF788246A3F8008048681F4608 +:10DF30000D4680788DF8060048680088ADF804002A +:10DF400000208DF80A00088A0C88A04200D30446FD +:10DF50002C8241E0288A401C2882701D6968FFF7E6 +:10DF60004FFDB8BB3988414501D1601E38806888B3 +:10DF7000A04236D3B178307901F0030140EA01299B +:10DF800001A9701DFFF73CFD20BB298941452CD01C +:10DF9000002231460798FFF74BFDD8B9298949453A +:10DFA00018D1E9680391B5F80AC0D6F808B0504610 +:10DFB000CDF800C004F050FEDDF800C05A460CF168 +:10DFC000070C1FFA8CFC4B460399CDF800C004F0F7 +:10DFD00000FA50B1641CA4B2204600F00FF906000C +:10DFE000B8D1641E2C820A20D0E67C807079B8718A +:10DFF000F088B8803178F07801F0030140EA012020 +:10E000007881A7F80C90504604F0BAFC324607F12C +:10E010000801FFF74DFD38610020B7E62DE9FF4FFD +:10E0200087B081461C469246DDF860B0DDF854802A +:10E03000089800F0E3F805000CD0484604F0A0FC76 +:10E040002978090608D57549897A814204D887203C +:10E050000BB0D6E50120FBE7CAF309062A4601A961 +:10E06000FFF726FD0746149807281CD000222946F2 +:10E07000FFF7DEFC0028EBD12878400613D50120FD +:10E080008DF808000898ADF80C00BDF80400ADF854 +:10E090000E00ADF81060ADF8124002A94846F9F73D +:10E0A00040F80028D4D12978E87801F0030140EA4B +:10E0B0000121AA78287902F0030240EA022056459D +:10E0C00007D0B1F5007F04D9611E814201DD0B202C +:10E0D000BEE7864201D90720BAE7801B85B2A54278 +:10E0E00000D92546BBF1000F01D0ABF800501798BE +:10E0F00018B1B9192A4617F041FAB8F1000F0DD03E +:10E100003E4448464446169F04F0B8FD2146FF1D94 +:10E11000BCB232462B46009404F0C5F9002097E7C4 +:10E120002DE9F04107461D461646084600F066F800 +:10E1300004000BD0384604F023FC2178090607D5EB +:10E140003649897A814203D8872012E5012010E5FB +:10E1500022463146FFF7ACFC65B12178E07801F04A +:10E16000030140EA0120B0F5007F01D8012000E062 +:10E17000002028700020FCE42DE9F04107461D46F0 +:10E180001646084600F03AF804000BD0384604F072 +:10E19000F7FB2178090607D52049897A814203D8FF +:10E1A0008720E6E40120E4E422463146FFF7AEFC96 +:10E1B000FF2D14D02178E07801F0030240EA02201C +:10E1C00040F20122B0FBF2F302FB130015B900F29A +:10E1D000012080B2E070000A60F30101217000208C +:10E1E000C7E410B50C4600F009F828B1C1882180B9 +:10E1F0004079A070002010BD012010BD0749CA88D9 +:10E20000824209D340B1096800EB40006FF00B0275 +:10E2100002EB80000844704700207047F80500209A +:10E2200010B508F0EFFAF4F741FB08F051F9BDE83A +:10E23000104008F019BA302834BF01200020704780 +:10E24000202834BF4FF0A0420C4A012300F01F00E9 +:10E2500003FA00F0002914BFC2F80C05C2F8080543 +:10E260007047202834BF4FF0A041044900F01F0040 +:10E27000012202FA00F0C1F81805704700030050AF +:10E2800070B50346002002466FF02F050EE09C5C3F +:10E29000A4F130060A2E02D34FF0FF3070BD00EB20 +:10E2A000800005EB4000521C2044D2B28A42EED3DB +:10E2B00070BD30B50A230BE0B0FBF3F403FB14048C +:10E2C000B0FBF3F08D183034521E05F8014CD2B279 +:10E2D000002AF1D130BD30B500234FF6FF7510E0B4 +:10E2E000040A44EA002084B2C85C6040C0F303140E +:10E2F000604005EA00344440E0B25B1C84EA401010 +:10E300009BB29342ECD330BD2DE9F041FA4B00268D +:10E31000012793F860501C7893F864C0B8B183F873 +:10E320008D40A3F88E1083F88C2083F88A70BCF19E +:10E33000000F0CBF83F8906083F89050EF4880681E +:10E34000008804F089FCBDE8F04104F01FB94FF6E5 +:10E35000FF7083F88D40A3F88E0083F88C2083F83B +:10E360008A70BCF1000F14BF83F8905083F890605E +:10E37000BDE8F08170B5E14E0446306890F8981021 +:10E380000025012919D090F89210012924D090F885 +:10E39000681001292AD090F88A1001291CBF00209A +:10E3A00070BD657017212170D0F88C106160B0F8D5 +:10E3B0009010218180F88A5016E065701C21217030 +:10E3C000D0F899106160D0F89D10A16090F8A1106C +:10E3D000217380F8985007E0657007212170D0F80C +:10E3E0009410616080F89250012070BD6570142116 +:10E3F000217000F16A012022201D17F0BFF80121D1 +:10E400002172306880F86850BB48B0F86C20A0F8E2 +:10E410009420B268537B80F8963080F89210108870 +:10E4200004F01AFC04F0C1F8DEE7B448006890F884 +:10E430006810002914BFB0F86C004FF6FF707047E9 +:10E4400070B5AE4C06462068002808BFFFDF0025E7 +:10E45000206845706660002808BFFFDF20684178AB +:10E4600000291CBFFFDF70BDA42117F028F9206828 +:10E47000FF2101707F2180F836101321418428216B +:10E4800080F86510012180F8581080F85D5008F080 +:10E4900082FEBDE8704008F048B8984909680978DC +:10E4A00081420CBF0120002070479448006890F81A +:10E4B0002200C0F3400070479048006890F82200A6 +:10E4C00000F0010070478D48006890F82200C0F30A +:10E4D000001070472DE9F04388480024016891F846 +:10E4E0002400B1F822C0C0F38002C0F340031A44F4 +:10E4F00000F001000244CCF3001060B3BCF1130F34 +:10E5000021D00BDCBCF1100F02BF7D4830F81200A7 +:10E51000BDE8F083BCF1120F15D008E0BCF1150F77 +:10E5200009D0BCF11D0F04BF7648BDE8F083FFDFC2 +:10E530002046BDE8F0837449002031F8121012FB28 +:10E540000010BDE8F0837149002031F8121012FB71 +:10E550000010BDE8F08391F85A3091F85B002E2648 +:10E560004FF47A774FF014084FF04009022B04BFA4 +:10E570004AF2D745B5FBF7F510D0012B04BF4AF29C +:10E580002F75B5FBF7F510D04AF62315B5FBF7F557 +:10E59000082B08BF4E4613D0042B18D02646082B54 +:10E5A0000ED0042B13D0022B49D004F12806042BE3 +:10E5B0000FD0082B1CBF4FF01908082304D00AE025 +:10E5C0004FF0140806F5A8764FF0400303E006F577 +:10E5D000A8764FF0100318FB036313FB0253C2EB42 +:10E5E00002124B4D02EB820205EB82021A441CF030 +:10E5F000010F4FF4C8734FF4BF7504BFCCF340064E +:10E60000002E77D0CCF3400602F5A572EEB10828B3 +:10E6100004BF1E4640270CD0042804BF2E461027F6 +:10E6200007D0022807BF04F11806042704F12806C2 +:10E63000082707EB870808EB87173E441BE004F127 +:10E6400018064FF019080423C5E7082804BF1E4622 +:10E6500040270CD0042804BF2E46102707D00228DC +:10E6600007BF04F11806042704F12806082707EB62 +:10E67000871706EB8706324402F19C0691F8652065 +:10E6800010F00C0F08BF00223244082804BF1E46B9 +:10E6900040270CD0042804BF2E46102707D002289C +:10E6A00007BF04F11806042704F128060827C7EB62 +:10E6B000C70707EB470706EB4706324498321CF0C2 +:10E6C000010F27D0082808BF40200CD0042804BF21 +:10E6D0002B46102007D0022807BF04F1180304209E +:10E6E00004F12803082000EB400101EB001018445E +:10E6F00002444AE04DE000000406002060000020D3 +:10E70000285B02008E891300305B0200205B020050 +:10E71000D4FEFFFF082804BF9C4640260CD00428E6 +:10E7200004BFAC46102607D0022807BF04F1180C1E +:10E73000042604F1280C082606EB8616898F0CEBBC +:10E74000860C6244EB2920D944F2552C0B3101FB95 +:10E750000CF1890D082807D0042802D0022805D022 +:10E7600008E02B46102008E0402006E004F11803E2 +:10E77000042002E004F12803082000EB801003EBE2 +:10E78000800000F5A57001FB002202F26510BDE8D3 +:10E79000F08302F5A572082804BF9C4640260CD0E1 +:10E7A000042804BFAC46102607D0022807BF04F196 +:10E7B000180C042604F1280C082606EB8616B1F87E +:10E7C00044100CEB860C6244EB29DED944F2552C44 +:10E7D0000B3101FB0CF1890D0828C5D00428C0D0ED +:10E7E0000228C7D1C2E7FE4840F271210068806A62 +:10E7F00048437047FA48006890F83500002818BF71 +:10E800000120704710B5F74C207B022818BF032861 +:10E8100008D1207D04F115010DF0A1FC08281CBFD2 +:10E82000012010BD207B002816BF022800200120F7 +:10E83000BDE8104009F0C0B9EA4908737047E849DB +:10E84000096881F8300070472DE9F047E44C2168F1 +:10E85000087B002816BF022800200120487301F120 +:10E860000E0109F093F92168087B022816BF0328DE +:10E870000122002281F82F204FF0080081F82D009E +:10E88000487B01F10E034FF001064FF0000701280D +:10E8900004BF5B7913F0C00F0AD001F10E03012809 +:10E8A00004D1587900F0C000402801D0002000E0D9 +:10E8B000012081F82E00002A04BF91F8220010F0F8 +:10E8C000040F07D0087D01F115010DF048FC216807 +:10E8D00081F82D002068476006F0CEF92168C14D0F +:10E8E0004FF00009886095F82D000DF054FC80462B +:10E8F00095F82F00002818BFB8F1000F04D095F844 +:10E900002D000DF00FFA68B195F8300000281CBFFB +:10E9100095F82E0000281DD0697B05F10E00012915 +:10E920000ED012E06E734A4605F10E01404609F022 +:10E9300082F995F82D1005F10E000DF023FD09E088 +:10E94000407900F0C000402831D0394605F10E0072 +:10E9500009F0A8F92068C77690F8220010F0040F9B +:10E9600008BFBDE8F087002795F82D000DF08EFA5E +:10E97000050008BFBDE8F08710210EF04CFA002812 +:10E9800018BFBDE8F08720683A4600F11C01C67642 +:10E99000284609F050F9206800F11C0160680EF06B +:10E9A00093FE6068BDE8F04701210EF0A8BE0DF0AF +:10E9B00026FD4A4605F10E0109F03DF9CAE7884AED +:10E9C0001268137B0370D2F80E000860508A8880AA +:10E9D000704778B583490446814D407B08732A68A7 +:10E9E000207810706088ADF8000080B200F001015E +:10E9F000C0F3400341EA4301C0F3800341EA8301CD +:10EA0000C0F3C00341EAC301C0F3001341EA03119C +:10EA1000C0F3401341EA4311C0F3801041EA801073 +:10EA20005084E07D012808BF012607D0022808BFD6 +:10EA3000022603D0032814BFFFDF0826286880F8C9 +:10EA40005A60607E012808BF012607D0022808BF4F +:10EA5000022603D0032814BFFFDF0826286880F8A9 +:10EA60005B60217B80F82410418C1D290CBF0021A4 +:10EA700061688162617D80F83510A17B002916BF35 +:10EA80000229002101210175D4F80F10C0F81510DA +:10EA9000B4F81310A0F81910A17EB0F8662061F345 +:10EAA0000302A0F86620E17E012918BF002180F84A +:10EAB0003410002078BD4A480068408CC0F3001133 +:10EAC00031B1C0F38000002804BF1F20704702E06E +:10EAD000C0F3400109B10020704710F0010F14BFCE +:10EAE000EE20FF2070473E480068408CC0F30011C4 +:10EAF00019B1C0F3800028B102E0C0F3400008B1B2 +:10EB000000207047012070473549002209680A66D5 +:10EB10004B8C1D2B0CBF81F8642081F8640070477A +:10EB200000232F4A126882F859309164A2F84C00F1 +:10EB3000012082F859007047294A0023126882F8A0 +:10EB40005830A2F854000120116582F8580070472F +:10EB50002349096881F85D0070472148006890F9F1 +:10EB60005D0070471E48006890F82200C0F3401016 +:10EB700070471B48006890F82200C0F3C00070473F +:10EB8000012070471648006890F85B00704770B528 +:10EB900008F0EBFA08F0CAFA08F0A2F908F020FA37 +:10EBA0000F4C2068016E491C016690F83300002567 +:10EBB00030B108F0F0FA07F0B8FC206880F8335064 +:10EBC0002068457090F8371021B1BDE870400420EE +:10EBD00009F0D7BC90F8641001B3006E814203E0E5 +:10EBE000600000200406002018D8042009F0C9FCA9 +:10EBF000206890F8220010F0010F07D0A06843228F +:10EC00000188BDE870400120FFF77EBBBDE8704081 +:10EC100043224FF6FF710020FFF776BBBDE870403E +:10EC2000002009F0AEBC2DE9F04782B00F468146C6 +:10EC3000FE4E4FF000083068458C15F0030F10D0E1 +:10EC400015F0010F05F0020005D0002808BF4FF0B5 +:10EC5000010806D004E0002818BF4FF0020800D1D8 +:10EC6000FFDF4FF0000A544615F0010F05F00200D7 +:10EC70000DD080B915F0040F0DD04AF00800002F18 +:10EC80001CBF40F0010040F0020440D08FE010B102 +:10EC900015F0040F0DD015F0070F10D015F0010F6F +:10ECA00005F0020036D0002808BF15F0040F27D069 +:10ECB0003DE0002F18BF4AF0090478D134E02FB1AD +:10ECC0004AF0080415F0200F14D070E0316805F008 +:10ECD0002002B1F84400104308BF4AF0010466D096 +:10ECE0004AF0180415F0200F61D191F85A10082944 +:10ECF00059D155E0316891F85A10082950D152E0A5 +:10ED00004AF00800002F18BF40F001044FD140F036 +:10ED100010044CE0002818BF15F0040F07D0002F96 +:10ED200018BF4AF00B0442D14AF018043FE015F036 +:10ED3000030F3BD115F0040F38D077B131684AF09A +:10ED4000080091F85A1008290CBF40F0020420F086 +:10ED5000020415F0200F21D029E0316805F02002CF +:10ED6000B1F84400104308BF4AF003041FD04AF032 +:10ED7000180015F0200F08D091F85A10082914BF78 +:10ED800040F0020420F0020411E091F85A20082A11 +:10ED900014BF40F0010020F00100EDE7082902D087 +:10EDA00024F0010403E044F0010400E0FFDF15F06B +:10EDB000400F1BD0C7B93168B1F84400002804BF28 +:10EDC000488C10F0010F0BD110F0020F08BF10F0AB +:10EDD000200F05D115F0010F08BF15F0020F03D069 +:10EDE00091F85A00082801D044F040047068A0F857 +:10EDF00000A0017821F02001017007210EF030FC05 +:10EE0000414670680EF023FE214670680EF02BFE1E +:10EE100014F0010F0AD006230022854970680EF015 +:10EE2000FCFD3068417B70680EF05CFC14F0020F52 +:10EE300018D0D6E90010B9F1000F4FF006034FF0DB +:10EE4000010207D01C310EF0E8FD012170680EF0C0 +:10EE500056FC07E015310EF0E0FD3068017D70686A +:10EE60000EF04DFC14F0040F18BFFFDF14F0080F74 +:10EE700017D0CDF800A03068BDF800100223B0F81C +:10EE80006600020962F30B01ADF800109DF8011055 +:10EE9000032260F307118DF80110694670680EF0C7 +:10EEA000BCFD012F61D13068B0F84410E9B390F88F +:10EEB0002200C0F34000C0BB70680EF0C4FD401CCF +:10EEC000C7B23068B0F84420B0F85610551AC7F1F0 +:10EED000FF018D42A8BF0D46AA423AD990F8220000 +:10EEE00010F0010F35D144F01004214670680EF087 +:10EEF000BAFDF81CC0B2ED1E284482B23068B0F8EA +:10EF00006610036E090951FA83F190F85C30494F9D +:10EF10001944BC460023E1FB07C31B096FF0240C16 +:10EF200003FB0C1180F85C1000E01EE090F85B0021 +:10EF3000012101F037F80090BDF800009DF80210A3 +:10EF4000032340EA01400190042201A970680EF0F9 +:10EF500064FD3068AAB2016C70680EF0B2FD3068D2 +:10EF6000B0F856102944A0F8561014F0400F06D0FF +:10EF7000D6E90010012306225D310EF04EFD14F09B +:10EF8000200F18BFFFDF0020002818BFFFDF02B0EE +:10EF9000BDE8F0872DE9F843244C2068002808BF1D +:10EFA000FFDF2068417839BB0178FF2924D0002693 +:10EFB00080F83160A0F85660867080F8376030467F +:10EFC00008F022F807F0E2FC206890F95D0007F0F5 +:10EFD00082FD194807F085FD184807F0FBFF6068BF +:10EFE00008F015F8206890F8240010F0010F06D002 +:10EFF000252007F07EFD09E00C20BDE8F88310F025 +:10F00000020F18BF262075D007F073FD206890F816 +:10F010005A10252007F078FC206880F82C6007F053 +:10F02000EDFF206890F85A10002009E060000020F1 +:10F030001206002053E4B36E1C5B0200195B020051 +:10F0400007F04BFE0F21052007F019FD206890F80E +:10F050002E10002901BF90F82F10002990F82200EF +:10F0600010F0040F75D005F007FE0546206829460C +:10F07000806806F01AFBDFF83084074690FBF8F052 +:10F0800008FB10704142284605F0F7FA21688860B5 +:10F0900097FBF8F04A68104448600DF05DF80146AF +:10F0A0002068426891426FD8C0E90165FF4D4FF07A +:10F0B000010895F82D000DF06EF8814695F82F00A7 +:10F0C0000127002818BFB9F1000F04D095F82D00D2 +:10F0D0000CF028FEA8B195F8300000281CBF95F868 +:10F0E0002E00002825D0697B05F10E00012916D0DD +:10F0F0001AE0FFE710F0040F14BF2720FFDF83D1D1 +:10F1000084E73A466F7305F10E01484608F093FD17 +:10F1100095F82D1005F10E000DF034F909E0407955 +:10F1200000F0C000402815D0414605F10E0008F05F +:10F13000B9FD206890F8220010F0040F24D095F853 +:10F140002D000CF0A3FE05001ED010210DF063FE73 +:10F1500040B119E00DF053F93A4605F10E0108F0FF +:10F160006AFDE6E720683A4600F11C01C7762846AA +:10F1700008F061FD206800F11C0160680EF0A4FA3F +:10F18000012160680EF0BBFA2068417B0E3007F069 +:10F190005AFC206890F8581059B3B0F85410A0F8F1 +:10F1A0004410016D016490F82210C1F30011E9B917 +:10F1B000B0F8660002210509ADF80050684606F077 +:10F1C0003DFE28B1BDF80000C0F30B00A84204D1F9 +:10F1D000BDF80000401CADF800002168BDF800003B +:10F1E000B1F8662060F30F12A1F86620206880F85D +:10F1F0005860206890F8591031B1B0F84C108187F0 +:10F20000816C816380F85960B0F86610026E09095C +:10F2100051FA82F190F85C20DFF894C21144634601 +:10F220000022E1FB0C3212096FF0240302FB0311F0 +:10F2300080F85C100DF013F8032160680DF092F86F +:10F24000216881F833000020BDE8F883994988607F +:10F2500070472DE9F043974C83B0226892F8313023 +:10F260003BB1508C1D2808BFFFDF03B0BDE8F04361 +:10F270008DE401260027F1B1054692F85C0007F005 +:10F2800038FC206890F85B10FF2007F03DFB2068F9 +:10F290004FF4A57190F85B20002007F0E4FD206892 +:10F2A00090F8221011F0030F00F02E81002D00F0D5 +:10F2B000258100F029B992F822108046D07EC1F352 +:10F2C0000011002956D0054660680780017821F0BA +:10F2D00020010170518C132937D01FDC102908BF81 +:10F2E000022144D0122908BF062140D0FFDF6F4D14 +:10F2F000606805F10E010EF0D9F9697B60680EF0C7 +:10F30000F1F92068418C1D2918BF152965D0B0F886 +:10F310004420016C60680EF0FEF95EE0152918BF0C +:10F320001D29E3D14FF001010EF09AF960680178D0 +:10F3300041F020010170216885B11C310EF0C4F943 +:10F34000012160680EF0DBF9D1E700210EF088F9A9 +:10F350006068017841F020010170C8E715310EF0B6 +:10F36000B3F92068017D60680EF0C9F9BFE70EF0BF +:10F3700077F9BCE70021FFF756FC6068C17811F00F +:10F380003F0F2AD0017911F0100F26D00EF066F948 +:10F390002368024693F82410C1F38000C1F3400CA7 +:10F3A000604401F0010100EB010C93F82C10C1F353 +:10F3B0008000C1F34005284401F001010844ACEB92 +:10F3C0000000C1B293F85A0000F0ECFD0090032356 +:10F3D0000422694660680EF020FB2068002590F842 +:10F3E000241090F82C0021EA000212F0010F18BF3F +:10F3F00001250ED111F0020F04D010F0020F08BF4A +:10F40000022506D011F0040F03D010F0040F08BF3E +:10F410000425B8F1000F2BD0012D1BD0022D08BF01 +:10F4200026201BD0042D14BFFFDF272016D0206814 +:10F4300090F85A10252007F067FA206890F82210FB +:10F44000C1F3001169B101224FF49671002007F059 +:10F450000AFD0DE0252007F04CFBE8E707F049FB2B +:10F46000E5E790F85A204FF49671002007F0FBFC76 +:10F47000206890F82C10294380F82C1090F8242054 +:10F4800032EA01011DD04670418C13292CD027DCB3 +:10F49000102904BF03B0BDE8F083122924D000BFB7 +:10F4A000C1F30010002807E040420F0004060020CE +:10F4B00053E4B36E6000002018BFFFDF03B0BDE867 +:10F4C000F083418C1D2908BF80F82C70DBD0C1F37C +:10F4D0000011002914BF80F8316080F83170D2E744 +:10F4E000152918BF1D29DBD190F85A2003B04FF021 +:10F4F0000101BDE8F043084607F092BE90F85B209A +:10F500000121084607F08CFE2168002DC87E7CD0C2 +:10F510004A8C3D46C2F34000002808BF47F008056A +:10F5200012F0400F18BF45F04005002819BFD1F870 +:10F530003890B1F83C80D1F84090B1F844806068D0 +:10F54000072107800EF08CF8002160680EF07FFA2A +:10F55000294660680EF087FA15F0080F15D020686C +:10F56000BDF800100223B0F86600020962F30B0137 +:10F57000ADF800109DF80110032260F307118DF81B +:10F580000110694660680EF048FA60680EF024F9D0 +:10F590002168C0F1FE00B1F85620A8EB02018142BB +:10F5A000A8BF0146CFB2D019404542D245F0100164 +:10F5B00060680EF058FA60680EF00EF92168C0F12C +:10F5C000FE00B1F85610A8EB01018142A8BF014628 +:10F5D000CFB260680EF037FA3844421C2068B0F8A9 +:10F5E0006610036E090951FA83F190F85C30FF4D03 +:10F5F0001944AC460023E1FB05C31B096FF0240C42 +:10F6000003FB0C1180F85C1000E038E090F85B0020 +:10F61000012100F0C7FC0090BDF800009DF8021029 +:10F62000032340EA01400190042201A960680EF022 +:10F63000F4F9216891F8220010F0400F05D0012361 +:10F6400006225D3160680EF0E8F920683A46B0F8AD +:10F65000560000EB090160680EF033FA2068B0F83C +:10F6600056103944A0F8561008F0C1F9002818BF08 +:10F67000FFDF20684670867003B0BDE8F08301218B +:10F68000FFF7D1FAF0E7DA4810B50068417841B9E0 +:10F690000078FF2805D000210846FFF7DAFD00209A +:10F6A00010BD07F062FD07F041FD07F019FC07F0FF +:10F6B00097FC0C2010BD10B5CD4C206890F82200AE +:10F6C00010F0010F1CBFA06801884FF03C0212BF70 +:10F6D00001204FF6FF710020FEF716FE2168012081 +:10F6E00081F8370010BDC249096881F832007047BF +:10F6F0002DE9F041002508F010FF002800F00581F9 +:10F70000BB4C2068417801270026012906D0022938 +:10F7100001D003297ED0FFDFBDE8F0818178022689 +:10F720000029418C46D0C1F34002002A08BF11F0E5 +:10F73000010F70D090F85B204FF001014FF00000F6 +:10F7400007F06EFD216891F82200C0F34000002808 +:10F7500014BF0C20222091F85B1007F0D5F8206828 +:10F76000467090F8330058B106F0CBFE206890F850 +:10F770005B0010F00C0F0CBF4020452007F001FD8E +:10F78000206890F83400002818BF07F019FD2168A0 +:10F7900091F85B0091F8651010F00C0F08BF002184 +:10F7A000962007F055FC08F019F9002818BFFFDF74 +:10F7B000BDE8F081C1F3001282B110293FD090F86A +:10F7C000330020B106F09DFE402007F0DAFC2068EF +:10F7D00090F8221011F0040F36D043E090F8242066 +:10F7E00090F82C309A422AD1B0F84400002808BF83 +:10F7F00011F0010F05D111F0020F08BF11F0200F19 +:10F800007ED04FF001014FF00000FFF722FD20688D +:10F81000418C01E040E034E011F0010F04BFC1F37E +:10F820004001002907D1B0F85610B0F844209142A9 +:10F8300018BFBDE8F08180F83170BDE8F081BDE807 +:10F84000F0410021012004E590F83510012914BF92 +:10F850000329102545F00E0190F85A204FF00000C2 +:10F8600007F0DEFC206890F83400002818BF07F08D +:10F87000A7FC0021962007F0EBFB20684670BDE84E +:10F88000F081B0F85610B0F8440081423DD0BDE898 +:10F89000F04101210846DCE48178D9B1418C11F0B6 +:10F8A000010F1CD080F8687090F86A20B0F86C10D6 +:10F8B0000120FEF729FD2068467007F056FC07F08E +:10F8C00035FC07F00DFB07F08BFBBDE8F041032092 +:10F8D00008F057BE8178BDE8F0410120B9E411F08D +:10F8E000020F04BFFFDFBDE8F081B0F85610808F33 +:10F8F00081420AD001210846FFF7ABFC032000E05B +:10F9000003E021684870BDE8F081BDE8F041FFF7F1 +:10F910003EB9FFF73CB910B5354C206890F834106B +:10F9200049B1363007F05BFC18B921687F2081F8B7 +:10F93000360007F03BFC206890F8330018B107F060 +:10F940002AFC06F0F2FD08F0E8FDA8B1206890F866 +:10F950002210C1F3001179B14078022818BFFFDFEF +:10F9600000210120FFF775FC2068417800291EBFA7 +:10F9700040780128FFDF10BDBDE81040FFF707B950 +:10F980002DE9F0471A4C0F4680462168B8F1030F65 +:10F99000488C08BFC0F3400508D000F0010591F87D +:10F9A0003200002818BF4FF0010901D14FF00009C3 +:10F9B00007F093F80646B8F1030F0CBF4FF00208AA +:10F9C0004FF0010835EA090008BFBDE8F08720685C +:10F9D00090F8330090B10CF025FC38700146FF28F8 +:10F9E0000CD06068C01C0CF0F6FB03E053E4B36E6F +:10F9F0006000002038780CF022FC06436068017833 +:10FA0000C1F3801221680B7D9A4208D10622C01CE6 +:10FA1000153115F087FD002808BF012000D0002017 +:10FA20003978FF2906D0C8B9206890F82D0088429F +:10FA300016D113E0A0B1616811F8030BC0F3801078 +:10FA40000CF08DFB05460CF0EDFC38B128460CF0AF +:10FA50001DFA18B110210DF0DEF908B1012000E007 +:10FA60000020216891F8221011F0040F01D0F0B1AC +:10FA70001AE0CEB9FE4890F83500002818BF40457E +:10FA800015D1616811F8030BC0F380100CF067FB0F +:10FA900004460CF0C7FC38B120460CF0F7F918B159 +:10FAA00010210DF0B8F910B10120BDE8F087002059 +:10FAB000BDE8F0872DE9F04FEE4D074683B028688A +:10FAC00000264078022818BFFFDF28684FF07F0922 +:10FAD00090F8341049B1363007F081FB002804BF9C +:10FAE000286880F8369007F061FB68680DF0DAFD51 +:10FAF0000446002F00F0048268680DF05EFF0028C5 +:10FB000000F0FE8106F0B7FF002800F0F981FF2029 +:10FB1000DFF864B3DFF8588300274FF0010A062CA2 +:10FB200080F00082DFE804F0EFEFEF03EFF78DF8ED +:10FB3000000069460320FFF723FF002800F0E4805F +:10FB4000296891F8340010B191F89800D0B1286874 +:10FB5000817801294CD06868042107800DF080FD70 +:10FB600008F10E0168680DF0A1FD98F80D106868A5 +:10FB70000DF0B8FD2868828F816B68680DF0EFFD8D +:10FB800000F04DB99DF8000081F898A00A7881F83E +:10FB90009920FF280FD001F19B029A310CF004FB51 +:10FBA000002808BFFFDF286890F89A1041F0020192 +:10FBB00080F89A100DE068680278C2F3801281F82C +:10FBC0009A20D0F80320C1F89B20B0F80700A1F8D4 +:10FBD0009F00286800F1A10490F836007F2808BF34 +:10FBE000FFDF286890F83610217080F83690AEE775 +:10FBF00090F822009BF80490C0F38014686864F3C6 +:10FC00008619072107800DF02BFD002168680DF093 +:10FC10001EFF494668680DF026FF0623002208F102 +:10FC20000E0168680DF0F9FE2868417B68680DF0E8 +:10FC300059FD68680DF0D0FD29688A8FC0F1FE017A +:10FC40008A42B8BF1146CFB2BA423DD9F81EC7B2F8 +:10FC500049F0100A514668680DF005FF68680DF01C +:10FC6000F2FE3844431C2868B0F86610026E090999 +:10FC700051FA82F190F85C20DFF800920A44C846FD +:10FC80004FF0000CE2FB098C4FEA1C116FF0240CC2 +:10FC900001FB0C2180F85C1090F85B001A460121F2 +:10FCA00000F080F90190BDF804009DF806100323D0 +:10FCB00040EA01400290042202A968680DF0ADFEFE +:10FCC000514668680DF0CFFE34B1D5E9001001232C +:10FCD00006225D310DF0A1FE28683A46816B686806 +:10FCE0000DF0EFFE2868A0F85670818F8F420CBF90 +:10FCF0000121002180F8311007F079FE002818BF9B +:10FD0000FFDF8CE007E00DE128688078002840F0F4 +:10FD1000F98000F0F5B88DF8000068680178C1F34B +:10FD20008019D0F803100191B0F80700ADF8080071 +:10FD300069460520FFF724FE0028286873D08178E3 +:10FD4000002972D090F85BA0D5E90104D0F80F101B +:10FD5000C4F80E10B0F813106182417D2175817DC9 +:10FD60006175B0F81710E182B0F819106180B0F831 +:10FD70001B10A180B0F81D10E18000F11F0104F1FB +:10FD8000080015F0B0FD686890F8241001F01F011C +:10FD9000217690F82400400984F8740184F854A076 +:10FDA00084F855A0286890F8651084F8561090F8EB +:10FDB0005D0084F857009DF80010A86800F05BF91A +:10FDC000022008F0DEFB6868DBF800400DF1040A51 +:10FDD000078008210DF044FC002168680DF037FE13 +:10FDE000214668680DF03FFE0623002208F10E014F +:10FDF00068680DF012FE2868417B68680DF072FC9F +:10FE0000494668680DF07BFC06230122514668686C +:10FE10000DF003FE07F0EBFD002818BFFFDF032005 +:10FE20002968487070E066E0FFE76868AC684FF0EA +:10FE300001080278617BC2F3401211406173D0F86F +:10FE40000F10C4F80E10B0F813106182417D2175B7 +:10FE5000817D6175B0F81710E182B0F819106180EA +:10FE6000B0F81B10A180B0F81D10E18008E0000080 +:10FE70000406002060000020145B020053E4B36E0F +:10FE800000F11F0104F1080015F02DFD686890F8DD +:10FE9000241001F01F01217690F82400400984F815 +:10FEA000740184F8548084F85580286890F86510AF +:10FEB00084F8561090F85D0084F857009DF8001003 +:10FEC000A86800F0D8F8286880F868A090F86A2040 +:10FED000B0F86C100120FEF717FA2868477007F099 +:10FEE00044F907F023F906F0FBFF07F079F8012049 +:10FEF00008F047FB08E090F82200C0F3001008B1BA +:10FF0000012601E0FEF743FE286890F8330018B19F +:10FF100007F041F906F009FB66B100210120FFF767 +:10FF200098F910E0286890F82200C0F3001000282B +:10FF3000E8D0E5E728688178012904D190F85B10C2 +:10FF4000FF2006F0E1FC28684178002919BF4178BC +:10FF5000012903B0BDE8F08F4078032818BFFFDF08 +:10FF600003B0BDE8F08F70B57E4C06460D462068A4 +:10FF7000807858B106F07EFC21680346304691F83F +:10FF80005B202946BDE8704009F0C6B806F072FC57 +:10FF900021680346304691F85A202946BDE8704052 +:10FFA00009F0BAB878B50C4600210091082804BFC2 +:10FFB0004FF4C87040210DD0042804BF4FF4BF7027 +:10FFC000102107D0022807BF01F11800042101F118 +:10FFD00028000821521D02FB010562489DF800100F +:10FFE000006890F85C2062F3050141F040068DF84E +:10FFF000006090F85B00012828D002282DD0082846 +:020000040001F9 +:1000000018BFFFDF2FD000BF26F080008DF8000062 +:10001000C4EB041000EB80001E2101EB800005FB07 +:1000200004045148844228BFFFDF5048A0FB04105D +:10003000BDF80110000960F30C01ADF80110BDF826 +:1000400000009DF8021040EA014078BD9DF80200D2 +:1000500020F0E0008DF80200D6E79DF8020020F0C5 +:10006000E000203004E09DF8020020F0E000403085 +:100070008DF80200C8E72DE9F0413A4D04460E46DE +:10008000286890F86800002818BFFFDF002728685C +:1000900080F86A702188A0F86C106188A0F882103E +:1000A000A188A0F88410E188A0F8861094F8741153 +:1000B00080F8881090F82F1049B1427B00F10E01B2 +:1000C000012A04D1497901F0C001402934D090F8C7 +:1000D000301041B1427B00F10E01012A04BF497981 +:1000E00011F0C00F28D000F1760015F0F3FB68681E +:1000F000FF2E0178C1F380116176D0F80310C4F8A7 +:100100001A10B0F80700E08328681DD0C167E18BA2 +:10011000A0F8801000F17002511E30460CF044F837 +:10012000002808BFFFDF286890F86F1041F0020137 +:1001300080F86F10BDE8F081D0F80E10C0F876108E +:10014000418AA0F87A10D2E7C767A0F88070617E74 +:1001500080F86F10D4F81A100167E18BA0F87410C2 +:10016000BDE8F08160000020C4BF03008988888852 +:100170000178406829B190F8141190F8730038B9EB +:1001800001E001F0CDBD19B1042901D00120704773 +:100190000020704770B50C460546062102F02AFC87 +:1001A000606008B1002006E00721284602F022FC2A +:1001B000606018B101202070002070BD022070BD69 +:1001C0002DE9FC470C4606466946FFF7E3FF002889 +:1001D0007DD19DF8000050B1FEF727F9B0427CD0E8 +:1001E000214630460AF088F9002873D12DE00DF041 +:1001F000E7FEB04271D02146304613F027FB0028BD +:1002000068D1019D95F8D80022E0012000E000208F +:10021000804695F837004FF0010A4FF00009F0B121 +:1002200095F8380080071AD584F8019084F800A06A +:1002300084F80490E68095F839102172698F618105 +:10024000A98FA18185F8379044E0019D95F81401AC +:1002500058350028DBD1E87E0028D8D0D5E73046D5 +:1002600002F00CFD070000D1FFDF384601F01CFF53 +:1002700040B184F801900F212170E680208184F83C +:1002800004A027E0304602F0E7FC070000D1FFDFC2 +:10029000B8F1000F21D0384601F05DFFB8B19DF8EC +:1002A000000038B90198D0F800014188B14201D16D +:1002B00080F80090304607F0E8FB84F801900C21AC +:1002C000217084F80490E680297F217200E004E028 +:1002D00085F81B900120BDE8FC870020FBE71CB5DA +:1002E0006946FFF757FF00B1FFDF684601F024FDC4 +:1002F000FB4900208968A1F8DA001CBD2DE9FC410A +:1003000004460E46062002F01DFB0546072002F0BB +:1003100019FB2844C7B20025A8463E4417E02088B0 +:10032000401C80B22080B04202D34046A4F8008036 +:1003300080B2B84204D3B04202D20020BDE8FC81B2 +:100340006946FFF727FF0028F8D06D1CEDB2AE42DA +:10035000E5D84FF6FF7020801220EFE738B54FF652 +:10036000FF70ADF800000DE00621BDF8000002F0BE +:1003700053FB04460721BDF8000002F04DFB0CB111 +:1003800000B1FFDF00216846FFF7B8FF0028EBD07F +:1003900038BD70B507F0E6FB0BF0CDFCD14C4FF645 +:1003A000FF7600256683A683CFA0257001680079BB +:1003B000A4F14002657042F8421FA11C1071601C3C +:1003C00013F065FB25721B2060814FF4A471A1819D +:1003D000E08121820321A1740422E274A082E082E0 +:1003E000A4F13E00218305704680BD480C300570A5 +:1003F000A4F110000570468070BD70B5B84C16466B +:100400000D466060217007F027FBFFF7A7FFFFF79D +:10041000C0FF207810F0CDFFB5480EF07CFA2178AF +:10042000606813F0D9FA20780AF0D4FE284608F064 +:1004300010FCAF48FEF704F8217860680AF042F932 +:100440003146207813F0DAFDBDE870400BF073BC44 +:1004500010B501240AB1002010BD21B1012903D03B +:100460000024204610BD02210DF068FBF9E72DE9BC +:10047000F047040000D1FFDF9A4802211C3081467A +:10048000FFF73CFF00B1FFDF964D0620B5F81C805A +:1004900002F058FA0646072002F054FA3044C6B279 +:1004A000701CC7B2A88BB04228D120460DF0FEFCCC +:1004B000B0B1207818283FD1207901283CD1E088BC +:1004C000062102F097FA040000D1FFDF208807F030 +:1004D000DCFA2088062102F09FFA40B3FFDF2BE010 +:1004E000287860B300266670142020702021201D1B +:1004F00015F0E5F8022020712E701DE0B84217D1EA +:100500002046FDF737FFD0B12078172814D1207985 +:1005100068B1E088072102F06DFA40B1008807F069 +:10052000B4FAE088072102F077FA00B1FFDF03E0B8 +:100530002146FFF745FE10B10120BDE8F0870221FA +:100540004846FFF7DBFE10B9A98B4145AAD12046EA +:10055000BDE8F04713F098BD10B501F089FB08B174 +:100560000C2010BD0BF03AFC002010BD10B5044665 +:10057000007818B1012801D0122010BD01F089FBCC +:1005800020B10BF0DBFD08B10C2010BD207801F08C +:1005900036FBE21D04F11703611CBDE810400BF0AF +:1005A000C2BC10B5044601F063FB08B10C2010BDBD +:1005B000207828B1012803D0FF280BD0122010BDCD +:1005C00001F01DFB611C0BF0C9FB08B1002010BD40 +:1005D000072010BD01200BF0FBFBF7E710B50BF077 +:1005E000B0FD08B1002010BD302010BD10B504468C +:1005F00001F04FFB08B10C2010BD20460BF09BFD15 +:10060000002010BD10B501F044FB20B10BF096FDA9 +:1006100008B10C2010BD0BF0EBFC002010BDFF2139 +:1006200081704FF6FF7181802D4949680A78827187 +:100630008A880281498841810121417000207047E8 +:100640007CB50025022A19D015DC12F10C0F15D04B +:1006500009DC12F1280F11D012F1140F0ED012F193 +:10066000100F11D10AE012F1080F07D012F1040F98 +:1006700004D04AB902E0D31E052B05D8012806D0C4 +:10068000022808D003280AD0122528467CBD10462F +:10069000FEF75EFAF9E710460EF0E8F8F5E70846CF +:1006A00014466946FFF776FD08B10225EDE79DF88F +:1006B00000000198002580F85740E6E710B5134682 +:1006C00001220CF0E5FB002010BD10B5044611F02E +:1006D00070FC05280ED0204610F05AFE002010BDF8 +:1006E0006C000020E8070020FFFFFFFF1F00000054 +:1006F000A80600200C20F2E710B5044601F0C9FA64 +:1007000008B10C20EBE72146002007F02CFA00206E +:10071000E5E710B5044610F0C9FE50B108F02AFD17 +:1007200038B1207808F0BBFA20780EF0DBFB00200F +:10073000D5E70C20D3E710B5044601F0AAFA08B1BA +:100740000C20CCE72146012007F00DFA0020C6E777 +:1007500038B504464FF6FF70ADF80000A079E17996 +:10076000884216D02079FDF766FD90B16079FDF7DB +:1007700062FD70B10022A079114614F0B3F840B9BF +:100780000022E079114614F0ADF810B9207A07285C +:1007900001D9122038BD08F0FAFC60B911F009FC4B +:1007A00048B900216846FFF7A9FD20B1204606F0B0 +:1007B00086F8002038BD0C2038BD2DE9FC41817839 +:1007C00005461A2925D00EDC16292DD2DFE801F0C6 +:1007D0002C2C2C2C2C212C2C2C2C2C2C2C2C2C2C64 +:1007E0002C2C2C2121212A291ED00BDCA1F11E0149 +:1007F0000C2919D2DFE801F0181818181818181861 +:100800001818180D3A3904290ED2DFE801F00D024C +:100810000D022888B0F5706F06D201276946FFF7F0 +:10082000B9FC18B1022089E5122087E59DF8000087 +:1008300001F0ECF9019C08B1FC3401E004F5BC7452 +:100840009DF8000001F0E2F9019E08B1FD3601E0DB +:1008500006F279166846FFF78BFC08B1207808B1DC +:100860000C206BE52770A8783070684601F064FAB8 +:10087000002063E57CB50D466946FFF78BFC00263A +:1008800018B12E602E7102207CBD9DF8000001F091 +:10089000BDF9019C9DF80000583401F0B7F90198AA +:1008A00084F8406081682960017B297194F84010C8 +:1008B0000029F5D100207CBD70B5044691F85500A3 +:1008C00091F856300D4610F00C0F00D1002321890D +:1008D000A0880CF0A1FC696A81421DD2401A401C1C +:1008E000A1884008091A8AB2A2802189081A2081A9 +:1008F000668895F8541010460CF035FC864200D2FC +:1009000030466080E68895F8551020890CF02BFC65 +:10091000864200D23046E08070BDF0B585B00D460D +:10092000064603A9FFF736FC00282DD19DF80C00E0 +:1009300060B300220499FB20B1F84A30FB2B00D3AE +:100940000346B1F84C40FB20FB2C00D30446DFF8F3 +:100950003CCC9CE8811000900197CDF808C0ADF820 +:100960000230ADF806406846FFF7A6FF6E80BDF87E +:100970000400E880BDF808006881BDF80200A88086 +:10098000BDF806002881002005B0F0BD0122D1E7A6 +:100990002DE9F04186B0044600886946FFF7FAFB6E +:1009A000002876D12189E08801F0D5F9002870D19E +:1009B000A188608801F0CFF900286AD12189E088F8 +:1009C00001F0D7F9002864D1A188608801F0D1F93D +:1009D00007005ED1208802A9FFF79FFF00B1FFDF6B +:1009E000BDF8101062880920914252D3BDF80C1056 +:1009F000E28891424DD3BDF81210BDF80E20238934 +:100A00001144A2881A44914243D39DF80010019DDD +:100A10004FF00008012640F6480041B185F8A36177 +:100A2000019991F8E61105F5D17541B91AE085F8FB +:100A30000D61019991F8301105F5867509B13A27D4 +:100A400024E0E18869806188E9802189814200D3BE +:100A50000146A980A188814200D20846288101224E +:100A600001990FE0E18869806188E98021898142EC +:100A700000D30146A980A188814200D2084628817E +:100A8000019900222846FFF717FF2E7085F8018094 +:100A9000384606B0BDE8F0817AE710B5044601F0AB +:100AA000F8F820B10BF04AFB08B10C2017E62078CB +:100AB00001F0A5F8E279611C0BF0C1FC08B100203F +:100AC0000DE602200BE610B503780446002B4068C3 +:100AD00013460A46014609D05FF001000CF0A5FB61 +:100AE0006168496A884203D90120F8E50020F5E7EA +:100AF0000020F4E52DE9F04117468A781E4680462D +:100B000042B11546C87838B10446690706D52AB1FE +:100B1000012104E00725F5E70724F6E70021620735 +:100B200002D508B1012000E00020014206D00122D8 +:100B300011464046FFF7C7FF98B93BE051B100228C +:100B400001214046FFF7BFFF58B9600732D50122A7 +:100B500011461FE058B1012200214046FFF7B3FFC4 +:100B600008B1092096E7680724D5012206E0680746 +:100B70004FEA44700AD5002813DB002201214046C9 +:100B8000FFF7A1FFB0B125F0040513E0002811DA4A +:100B9000012200214046FFF796FF58B124F00404DB +:100BA00008E0012211464046FFF78DFF10B125F005 +:100BB0000405F3E73D70347000206BE710B586B094 +:100BC0000446008803A9FFF7E5FA002806D1A088AB +:100BD00030B1012804D0022802D0122006B07EE5F0 +:100BE0006B4602AA214603A8FFF784FF0028F5D12F +:100BF0009DF80C3000220121002B049B06D083F8C5 +:100C0000AD11049B93F8FA316BBB24E083F8171104 +:100C1000049B93F83C313BB9049B93F816311BB904 +:100C2000049B93F87D300BB13A2010E0049B83F8CD +:100C30001611049B9DF8081083F81811049B9DF869 +:100C4000001083F81911049BA188A3F81A110499C4 +:100C500081F81721C2E7049B93F8AC311BB9049BC0 +:100C600093F87D300BB13A2010E0049B83F8AC116F +:100C7000049B9DF8081083F8AE11049B9DF80010AA +:100C800083F8AF11049BA188A3F8B011049981F8EF +:100C9000AD21A3E710B504460020A17801B90120D9 +:100CA000E2780AB940F0020001F06CF8002803D1A4 +:100CB0002046BDE8104081E711E570B51C460D46A1 +:100CC00018B1012801D0122070BD1946104601F05C +:100CD00069F830B12146284601F06EF808B10020CD +:100CE00070BD302070BD70B5044600780E460128F6 +:100CF00004D018B1022801D0032841D1607828B16E +:100D0000012803D0022801D0032839D1E07B10B993 +:100D1000A078012834D1A07830F0050130D110F04E +:100D2000050F2DD06289E188E0783346FFF7C5FFD3 +:100D3000002826D1A07805281ED16589A28921899D +:100D400020793346FFF7B9FF00281AD15FF0010080 +:100D500004EB40014A8915442218D37892789342D3 +:100D60000ED1CA8889888A420AD1401CC0B20228A2 +:100D7000EED3E088A84203D3A07B08B1072801D9AD +:100D8000122070BD002070BD10B586B0044600F082 +:100D900062FF08B10C2021E7022104F10A0001F0F2 +:100DA0001EF8A0788DF80800A0788DF80000607813 +:100DB0008DF8040020788DF80300A07B8DF80500E5 +:100DC000E07B00B101208DF80600A078C10717D0A4 +:100DD000E07800F0FBFF8DF80100E088ADF80A0034 +:100DE0006089ADF80C00A078400716D5207900F096 +:100DF000EDFF8DF802002089ADF80E00A0890AE011 +:100E000040070AD5E07800F0E1FF8DF80200E088A5 +:100E1000ADF80E006089ADF8100002A810F052FB8A +:100E20000028B8D168460EF062F8D7E610B504463F +:100E30000121FFF758FF002803D12046BDE81040EC +:100E4000A2E74CE40278012A01D0BAB118E0427856 +:100E50003AB1012A05D0022A12D189B1818879B12B +:100E600000E059B1418849B1808838B101EB810176 +:100E7000490000EB8000B1EB002F01D20020704749 +:100E80001220704770B5044600780D46012809D03D +:100E900011F08FF8052803D010F025FA002800D0B3 +:100EA0000C2070BD0DF0F0FE88B10DF002FF0DF0CA +:100EB000FBFF0028F5D125B160780DF08CFF0028EC +:100EC000EFD1A1886088BDE8704010F021BB1220EE +:100ED00070BD10B504460121FFF7B4FF002804D10E +:100EE0002046BDE810400121CCE704E42DE9F0479D +:100EF0000746B0F84C50FB2092460E46FB2D00D31F +:100F00000546DFF88C86B8F80A00A84200D20546EC +:100F100097F85510284600F08DFEB8F80C10814265 +:100F200000D208468146B7F84A40FB20FB2C00D38C +:100F30000446B8F80E00A04200D2044697F85410B8 +:100F4000204600F077FEB8F81010814200D2084623 +:100F50004FF4A4721B2C01D0904203D11B2D25D03D +:100F6000914523D0F580A6F808907480B080524651 +:100F700039463046FFF7A0FC01203070F0881B385E +:100F8000E02800D9FFDF70881B38E02800D9FFDF98 +:100F9000308940F64814A0F5A470A04200D9FFDFC4 +:100FA000B088A0F5A470A04200D9FFDFBDE8F087AB +:100FB000F0B5871FDDE9056540F67B44A74213D2F3 +:100FC0008F1FA74210D288420ED8B2F5FA7F0BD2FB +:100FD000A3F10A00241FA04206D2521C4A43B2EBDE +:100FE000830F01DAAE4201D90020F0BD0120F0BD2F +:100FF0002DE9FC47477A8946044617F0050F7DD056 +:10100000F8087BD194F83A0008B9012F76D1002571 +:10101000A8462E46F90789F0010A19D0208A5146C0 +:1010200000F0C0FEF0B36089514600F0C5FEC8B3C1 +:10103000208A6189884261D8A18EE08DCDE90001C6 +:10104000238D628CA18BE08AFFF7B2FF50B301259C +:10105000B8070ED504EB4500828EC18DCDE9001294 +:10106000038D428C818BC08AFFF7A2FFD0B1A846C6 +:101070006D1C78071ED504EB45065146308A00F0FA +:1010800091FE78B17089514600F096FE50B1308AD9 +:10109000718988425ED8B18EF08DCDE90001338D23 +:1010A000728C00E00AE0B18BF08AFFF781FF28B173 +:1010B0002E466D1CB9F1000F03D030E03020BDE8A2 +:1010C000FC87F80707D0780705D504EB460160894F +:1010D000498988423ED1228A01211BE0414503D043 +:1010E00004EB4100008A024404EB4100C38A868A73 +:1010F000B3422FD1838B468BB34200E02AE029D143 +:10110000438C068CB34225D1038DC08C834221D100 +:10111000491CC9B2A942E1D3608990421AD3207810 +:1011200010B1012816D10DE0A078B9F1000F07D059 +:1011300040B1012806D0022804D003280AD101E0DA +:101140000028EED1607838B1012805D0022803D0FC +:10115000032801D01220B2E70020B0E7002147E7C2 +:101160000178C90702D0406812F061BF12F02EBFAB +:101170002DE9F04788B00D46AFF69422D2E90092EF +:10118000014690462846FFF733FF06000CD100F0D9 +:1011900062FD40B9FE4F387828B90CF011FFA0F578 +:1011A0007F41FF3902D00C2008B0FFE6032105F192 +:1011B000100000F014FEF64801AA3E380190F548F0 +:1011C0000290F34806211038039007A801F0E0FBD5 +:1011D000040035D003210BF0BBFBB98AA4F84A10F8 +:1011E000FA8AA4F84C20FB7C0093BA46BB7C20888A +:1011F00001F0BBFC00B1FFDF208806F045FC218830 +:1012000004F10E0000F04FFDE3A004F112070068A6 +:1012100000900321684604F007FE002069460A5C3E +:101220003A54401CC0B20328F9D3A88B6080688C64 +:10123000A080288DE080687A410703D508270AE05E +:101240000920B1E7C10701D0012704E0800701D5DB +:10125000022700E000273A46BAF81800114610F0BD +:10126000EBF90146A062204610F0F4F917F00C0FDC +:1012700009D001231A46214600200BF0D6FF616AEF +:10128000884200D90926002784F85E7084F85F70D0 +:10129000A87800F0B4FC6076D5F80300C4F81A0012 +:1012A000B5F80700E083C4F8089084F80C800120AA +:1012B00084F80801024604F586712046FFF716FE01 +:1012C0008DF800700121684604F0AEFD9DF8000025 +:1012D00000F00701C0F3C1021144C0F340100844FC +:1012E0008DF80000401D2076092801D208302076B4 +:1012F000002120460BF02CFB68780DF0D0FCEEBBF3 +:10130000A9782878EA1C0DF092FC48B10DF0D1FCC8 +:10131000A9782878EA1C0DF038FD060002D052E0CA +:10132000122650E0687A00F005010020CA0700D0BC +:1013300001208A0701D540F00200490701D540F09D +:1013400008000DF05DFC06003DD1214603200DF0A4 +:1013500046FD060037D10DF04CFD060033D1697A09 +:1013600001F005018DF81010697AC90708D0688965 +:10137000ADF81200288AADF8140000E023E0012047 +:10138000697A8A0700D5401C490707D505EB40005C +:101390004189ADF81610008AADF8180004A810F0C5 +:1013A00091F8064695F83A0000B101200DF03AFC9C +:1013B0004EB90DF079FD060005D1A98F204610F039 +:1013C00023F8060008D0208806F05FFB208806215D +:1013D00001F022FB00B1FFDF3046E5E601460020C8 +:1013E000C6E638B56A48007878B910F0E2FD0528FD +:1013F00005D00CF0E5FDA0F57F41FF3905D068462A +:1014000010F0C9F8040002D00CE00C2038BD0098A0 +:10141000008806F03AFB00980621008801F0FCFAEB +:1014200000B1FFDF204638BD1CB582894189CDE976 +:1014300000120389C28881884088FFF7B9FD08B18E +:1014400000201CBD30201CBD70B50546FFF7ECFF29 +:1014500000280ED12888062101F0CCFA040007D01C +:1014600000F05EFC20B1D4F80001017831B901E050 +:10147000022070BDD4F84C11097809B13A2070BD32 +:1014800005218171D4F8001100200881D4F80011E1 +:10149000A8884881D4F80011E8888881D4F8001120 +:1014A0002889C881D4F80001028941898A4204D878 +:1014B0008279082A01D88A4201D3122070BD298876 +:1014C0004180D4F8001102200870002070BD3EB5A4 +:1014D00004460BF06FFCB0B12D480125A0F140028D +:1014E0004570236842F8423F23790021137141700F +:1014F0006946062001F007FA00B1FFDF684601F0F7 +:10150000E0F910B10EE012203EBDBDF80440029893 +:1015100080F80851684601F0D4F918B9BDF8040004 +:10152000A042F4D100203EBD70B5054600880621DA +:1015300001F060FA040007D000F0F2FB20B1D4F80B +:101540000011087830B901E0022070BDD4F84C01D8 +:10155000007808B13A2070BD9620005D10F0010FB0 +:1015600024D0D5F802004860D5F806008860D4F889 +:101570000001698910228181D4F8000105F10C0174 +:101580000E3004F5807413F0F9FF07E0385B0200B9 +:10159000E807002078000020112233002168032092 +:1015A0000870216828884880002070BD0C2070BD1C +:1015B00038B504460078EF284DD86088ADF80000B3 +:1015C000009800F01DFC88B36188080708D4D4E9AE +:1015D000012082423FD8202A3DD3B0F5804F3AD82F +:1015E000207B18B3072836D8607B28B1012803D0A8 +:1015F000022801D003282ED14A0703D4022801D0A3 +:10160000032805D1A07B08B1012824D1480707D4BD +:10161000607D28B1012803D0022801D003281AD107 +:10162000C806E07D03D5012815D110E013E001289C +:1016300001D003280FD1C80609D4607E012803D049 +:10164000022801D0032806D1A07E0F2803D8E07E0F +:1016500018B1012801D0122038BD002038BDF8B5DE +:1016600014460D46064607F092FD08B10C20F8BD61 +:101670003046FFF79DFF0028F9D1FDF76EFA28707C +:10168000B07554B9FF208DF8000069460020FDF7C1 +:1016900053FA69460020FDF743FA3046BDE8F840AA +:1016A000FDF797B90022DAE770B50C46054612B18E +:1016B0001F2907D80CE0FF2C04D8FCF704FF18B151 +:1016C0001F2C01D9122070BD2846FCF7E6FE08B198 +:1016D000002070BD422070BD10B50446408810B196 +:1016E000FDF701FA78B12078618800F00102607896 +:1016F000FFF7DAFF002805D1FDF7DDF962888242A5 +:1017000003D9072010BD122010BD10466168FDF7F7 +:1017100013FA002010BD10B50446408810B1FCF744 +:10172000C4FE70B12078618800F001026078FFF794 +:10173000BBFF002804D160886168FDF7F1F9002043 +:1017400010BD122010BD7CB504464078422501280A +:1017500008D8A078FCF7A1FE20B120781225012836 +:1017600002D090B128467CBDFDF703FA20B1A088D5 +:101770000028F7D08028F5D8FDF702FA60B160782C +:101780000028EFD02078012808D006F09DFA044602 +:1017900007F0BCF900287DD00C207CBDFDF732F8A5 +:1017A00010B9FDF7DFF990B307F0F1FC0028F3D191 +:1017B000FCF73BFEA0F57F41FF39EDD1FDF744F882 +:1017C000A68842F210704643A079FDF79DF9FCF718 +:1017D00073FEF8B10022072101A801F0D9F8040036 +:1017E00043D0FA480321846020460AF0B6FF204621 +:1017F000FDF72CFDF64DA88AA4F84A00E88AA4F863 +:101800004C00FCF760FE60B1288B01210DE0FFE782 +:1018100012207CBD3146002007F044FAD8B3FFDF28 +:101820004CE0FDF7AFF90146288B07F0F0FA0146CE +:10183000A0620022204606F04AFAFCF744FEB0B946 +:10184000FDF7A0F910F00C0F11D001231A46214624 +:1018500018460BF0EAFC616A884208D90721BDF8F6 +:10186000040001F0D9F800B1FFDF09207CBDE87C5D +:101870000090AB7CEA8AA98A208801F076F900B151 +:10188000FFDF208806F000F93146204607F00AFA0B +:1018900018B101E008E011E0FFDF002204F5D1718A +:1018A0002046FFF723FB09E044B1208806F0EDF85D +:1018B0002088072101F0B0F800B1FFDF00207CBDD7 +:1018C000002140E770B50D46072101F093F80400B0 +:1018D00003D094F87B0110B10AE0022070BD94F8A7 +:1018E0006500142801D0152802D194F8C80108B168 +:1018F0000C2070BD1022294604F5BE7013F03EFE88 +:10190000012084F87B01002070BD10B5072101F093 +:1019100071F818B190F87B1111B107E0022010BDE9 +:1019200090F86510142903D0152901D00C2010BDA2 +:10193000022180F87B11002010BD2DE9FC410C46EE +:101940004BF68032122194421DD8E4B16946FEF76D +:1019500021FC002815D19DF8000000F057F9019EE8 +:101960009DF80000583600F051F9019DAD1C2F88FC +:101970002246394630460AF0E6FE2888B842F6D1BB +:101980000020BDE8FC810846FBE77CB504460088E2 +:101990006946FEF7FFFB002810D19DF8000000F01B +:1019A00035F9019D9DF80000583500F02FF9019898 +:1019B000A27890F82C10914201D10C207CBD7F219F +:1019C0002972A9720021E972E17880F82D1021793D +:1019D00080F82E10A17880F82C1000207CBD1CB55A +:1019E0000C466946FEF7D6FB00280AD19DF8000098 +:1019F00000F00CF9019890F8730000B101202070FC +:101A000000201CBD7CB50D4614466946FEF7C2FB9E +:101A1000002809D19DF8000000F0F8F8019890F82E +:101A20002C00012801D00C207CBD9DF8000000F0A6 +:101A3000EDF8019890F86010297090F8610020701E +:101A400000207CBD70B50D461646072100F0D2FF80 +:101A500018B381880124C388428804EB4104AC4256 +:101A600017D842F210746343A4106243B3FBF2F23E +:101A7000521E94B24FF4FA72944200D91446A54211 +:101A800000D22C46491C641CB4FBF1F24A43521E9E +:101A900091B290F8B4211AB901E0022070BD01841E +:101AA0003180002070BD10B50C46072100F0A2FF68 +:101AB00048B180F8E74024B190F8E51009B107F08B +:101AC000BCF9002010BD022010BD017899B1417809 +:101AD00089B141881B290ED381881B290BD3C1886A +:101AE000022908D33A490268403941F8522F406828 +:101AF0004860002070471220704710B504460FF070 +:101B000097FD204607F052F9002010BD10B507F0F0 +:101B100050F9002010BD2DE9F04115460F4606464C +:101B20000122114638460FF087FD04460121384650 +:101B300007F06DF9844200D2044601213046653C2D +:101B400000F069F806460121002000F064F83044F6 +:101B500001219630844206D900F19601201AB0FB8B +:101B6000F1F0401C81B229800020BDE8F08110B561 +:101B7000044600F08EF808B10C2010BD601C0AF07D +:101B800039FC207800F00100FCF759FE207800F0C5 +:101B900001000DF089F8002010BD10B507F003F921 +:101BA000002010BD10B50446072000F0BDFE08B1AE +:101BB0000C2010BD2078C00716D000226078114696 +:101BC00012F090FE30B1122010BD00006C00002019 +:101BD000E8070020A06809F0D4F86078D4F8041071 +:101BE00009F0D8F80020EFE7002009F0CAF800213A +:101BF0000846F5E710B505F02BFB0020E4E718B127 +:101C0000022801D0012070470020704708B1002051 +:101C100070470120704710B5012904D0022905D072 +:101C2000FFDF2046D0E7C000503001E080002C30BC +:101C300084B2F6E711F00C0F04D04FF4747101EB8D +:101C4000801006E0022902D0C000703001E0800060 +:101C50003C3080B2704710B510F0ABF9042805D0C5 +:101C600010F0A7F9052801D00020ADE70120ABE76F +:101C700010B5FFF7F0FF10B10DF0DAF828B907F052 +:101C800086FA20B1FCF7B6FD08B101209CE70020E0 +:101C90009AE710B5FFF7DFFF18B907F078FA0028C8 +:101CA00092D0012090E72DE9FE4300250F468046A3 +:101CB0000A260421404604F0E0F840460BF01BF8E9 +:101CC000062000F03FFE044615E06946062000F0BD +:101CD0001AFE0AE0BDF80400B84206D002980422B9 +:101CE00041460E3013F01EFC50B1684600F0E9FD8D +:101CF0000500EFD0641E002C06DD002DE5D005E0C8 +:101D000040460BF001F8F5E705B9FFDFD8F8000011 +:101D10000BF015F8761E01D00028CAD0BDE8FE836E +:101D200090F8D81090F8730020B919B1042901D0A7 +:101D30000120704700207047017800290AD04168CF +:101D400091F8E520002A05D0002281F8E5204068BE +:101D500007F073B870471B38E12806D2B1F5A47FAD +:101D600003D344F29020814201D912207047002011 +:101D70007047FB2802D8B1F5296F01D911207047AF +:101D80000020704770B514460546012200F05CF84B +:101D9000002806D121462846BDE87040002200F008 +:101DA00053B870BD042803D321B9B0F5804F01D9D1 +:101DB0000020704701207047042803D321B9B0F5F3 +:101DC000804F01D90020704701207047012802D0C0 +:101DD00018B100207047022070470120704710B5ED +:101DE00000224FF4C84408E030F81230A34200D972 +:101DF000234620F81230521CD2B28A42F4D3E3E6D2 +:101E000080B2C1060BD401071CD481064FEAC07111 +:101E100001D5B9B900E099B1800713D410E04106AB +:101E200010D481060ED4C1074FEA807104D0002976 +:101E300002DB400704D405E0010703D4400701D4C6 +:101E400001207047002070470AB1012200E0022201 +:101E5000024202D1C80802D109B100207047112006 +:101E60007047000030B5058825F4004421448CB249 +:101E70004FF4004194420AD2121B92B21B339A4291 +:101E800001D2A94307E005F40041214303E0A21A6F +:101E900092B2A9431143018030BD08440830504339 +:101EA0004A31084480B2704770B51D4616460B464D +:101EB000044629463046049AFFF7EFFF0646B34230 +:101EC00000D2FFDF2821204613F0F9FB4FF6FF7008 +:101ED000A082283EB0B265776080B0F5004F00D98F +:101EE000FFDF618805F13C00814200D2FFDF60889E +:101EF0000835401B343880B220801B2800D21B20BC +:101F000020800020A07770BD8161886170472DE935 +:101F1000F05F0D46C188044600F12809008921F4CC +:101F2000004620F4004800F062FB10B10020BDE83C +:101F3000F09F4FF0000A4FF0010BB0450CD9617FC4 +:101F4000A8EB0600401A0838854219DC09EB0600A8 +:101F50000021058041801AE06088617F801B471A5C +:101F6000083F0DD41B2F00DAFFDFBD4201DC2946FC +:101F700000E0B9B2681A0204120C04D0424502DD36 +:101F800084F817A0D2E709EB06000180428084F8AC +:101F900017B0CCE770B5044600F12802C088E37D95 +:101FA00020F400402BB110440288438813448B4234 +:101FB00001D2002070BD00258A4202D301804580F5 +:101FC00008E0891A0904090C418003D0A01D00F023 +:101FD0001EFB08E0637F00880833184481B26288E2 +:101FE000A01DFFF73FFFE575012070BD70B50346EA +:101FF00000F12804C588808820F400462644A842C1 +:1020000002D10020188270BD98893588A84206D375 +:10201000401B75882D1A2044ADB2C01E05E02C1A55 +:10202000A5B25C7F20443044401D0C88AC4200D9EE +:102030000D809C8924B1002414700988198270BD18 +:102040000124F9E770B5044600F12801808820F4E6 +:1020500000404518208A002825D0A189084480B274 +:10206000A08129886A881144814200D2FFDF288834 +:10207000698800260844A189884212D1A069807F1E +:102080002871698819B1201D00F0C1FA08E0637F4A +:1020900028880833184481B26288201DFFF7E2FEC9 +:1020A000A6812682012070BD2DE9F04141898788F3 +:1020B0000026044600F12805B94218D004F10A08A8 +:1020C00021F400402844418819B1404600F09FFAAD +:1020D00008E0637F00880833184481B26288404674 +:1020E000FFF7C0FE761C6189B6B2B942E8D130462E +:1020F000BDE8F0812DE9F04104460B4627892830E0 +:10210000A68827F40041B4F80A8001440D46B7427E +:1021100001D10020ECE70AB1481D106023B1627FB5 +:10212000691D184613F02AFA2E88698804F1080000 +:1021300021B18A1996B200F06AFA06E0637F6288DC +:102140000833991989B2FFF78DFE474501D12089DF +:1021500060813046CCE78188C088814201D101206E +:1021600070470020704701898088814201D1012099 +:1021700070470020704770B58588C38800F1280437 +:1021800025F4004223F4004114449D421AD083896F +:10219000058A5E1925886388EC18A64214D313B10A +:1021A0008B4211D30EE0437F08325C1922444088F1 +:1021B00092B2801A80B22333984201D211B103E067 +:1021C0008A4201D1002070BD012070BD2DE9F04789 +:1021D0008846C1880446008921F4004604F1280796 +:1021E00020F4004507EB060900F001FA002178BB56 +:1021F000B54204D9627FA81B801A002503E06088DD +:10220000627F801B801A083823D4E28962B1B9F852 +:102210000020B9F802303BB1E81A2177404518DBBD +:10222000E0893844801A09E0801A217740450ADBAA +:10223000607FE1890830304439440844C01EA4F866 +:102240001280BDE8F087454503DB01202077E7E7F2 +:10225000FFE761820020F4E72DE9F74F044600F123 +:102260002805C088884620F4004A608A05EB0A06E3 +:1022700008B1404502D20020BDE8FE8FE08978B168 +:102280003788B6F8029007EB0901884200D0FFDFDB +:10229000207F4FF0000B50EA090106D088B33BE0E5 +:1022A0000027A07FB9463071F2E7E18959B1607F1C +:1022B0002944083050440844B4F81F1020F8031D86 +:1022C00094F821108170E28907EB080002EB080105 +:1022D000E1813080A6F802B002985F4650B1637F7A +:1022E00030880833184481B26288A01DFFF7BAFD18 +:1022F000E78121E0607FE1890830504429440844A7 +:102300002DE0FFE7E089B4F81F102844C01B20F837 +:10231000031D94F82110817009EB0800E28981B255 +:1023200002EB0800E081378071800298A0B1A01D07 +:1023300000F06DF9A4F80EB0A07F401CA077A07D3E +:1023400008B1E088A08284F816B000BFA4F812B0EB +:1023500084F817B001208FE7E0892844C01B30F8CB +:10236000031DA4F81F10807884F82100EEE710B553 +:10237000818800F1280321F400442344848AC28820 +:10238000A14212D0914210D0818971B9826972B193 +:102390001046FFF7E8FE50B91089283220F40040BB +:1023A000104419790079884201D1002010BD1846E7 +:1023B00010BD00F12803407F08300844C01E1060A3 +:1023C000088808B9DB1E136008884988084480B271 +:1023D00070472DE9F04100F12806407F1C46083087 +:1023E0009046431808884D88069ADB1EA0B1C01C91 +:1023F00080B2904214D9801AA04200DB204687B2F6 +:1024000098183A46414613F08DF8002816D1E01B83 +:1024100084B2B844002005E0ED1CADB2F61EE8E73A +:10242000101A80B20119A94206D83044224641460A +:10243000BDE8F04113F076B84FF0FF3058E62DE9D3 +:10244000F04100F12804407F1E46083090464318B2 +:10245000002508884F88069ADB1E90B1C01C80B208 +:10246000904212D9801AB04200DB304685B29918EA +:102470002A46404613F082F8701B86B2A84400201A +:1024800005E0FF1CBFB2E41EEAE7101A80B2811912 +:10249000B94206D821183246404613F06FF8A81901 +:1024A00085B2284624E62DE9F04100F12804407F5A +:1024B0001E46083090464318002508884F88069A23 +:1024C000DB1E90B1C01C80B2904212D9801AB0427B +:1024D00000DB304685B298182A46414613F04EF884 +:1024E000701B86B2A844002005E0FF1CBFB2E41EAA +:1024F000EAE7101A80B28119B94206D82044324660 +:10250000414613F03BF8A81985B22846F0E5401D76 +:10251000704710B5044600F12801C288808820F475 +:1025200000431944904206D0A28922B9228A12B9E6 +:10253000A28A904201D1002010BD0888498831B19B +:10254000201D00F064F800202082012010BD637F70 +:1025500062880833184481B2201DFFF783FCF2E73C +:102560000021C18101774182C1758175704703885F +:102570001380C28942B1C28822F4004300F12802CC +:102580001A440A60C08970470020704710B504469D +:10259000808AA0F57F41FF3900D0FFDFE088A0826C +:1025A000E08900B10120A07510BD4FF6FF71818256 +:1025B00000218175704710B50446808AA0F57F41DF +:1025C000FF3900D1FFDFA07D28B9A088A18A884209 +:1025D00001D1002010BD012010BD8188828A914266 +:1025E00001D1807D08B1002070470120704720F4A0 +:1025F000004221F400439A4207D100F4004001F464 +:102600000041884201D0012070470020704730B55A +:10261000044600880D4620F40040A84200D2FFDFA7 +:1026200021884FF4004088432843208030BD70B596 +:102630000C00054609D0082C00D2FFDF1DB1A1B265 +:10264000286800F044F8201D70BD0DB100202860FE +:10265000002070BD0021026803E0938812681944CD +:1026600089B2002AF9D100F032B870B500260D46C3 +:102670000446082900D2FFDF206808B91EE004469E +:1026800020688188A94202D001680029F7D1818899 +:102690000646A94201D100680DE005F1080293B297 +:1026A0000022994209D32844491B02608180216895 +:1026B000096821600160206000E00026304670BD9E +:1026C00000230B608A8002680A6001607047002363 +:1026D0004360021D018102607047F0B50F4601881A +:1026E000408815460C181E46AC4200D3641B30448B +:1026F000A84200D9FFDFA019A84200D9FFDF38198E +:10270000F0BD2DE9F041884606460188408815460F +:102710000C181F46AC4200D3641B3844A84200D9B1 +:10272000FFDFE019A84200D9FFDF708838447080CD +:1027300008EB0400BDE8F0812DE9F0410546008872 +:102740001E461746841B8846BC4200D33C442C805E +:1027500068883044B84200D9FFDFA019B84200D9D8 +:10276000FFDF68883044688008EB0400E2E72DE969 +:10277000F04106881D460446701980B21746884607 +:102780002080B84201D3C01B20806088A84200D2BC +:10279000FFDF7019B84200D9FFDF6088401B6080FE +:1027A00008EB0600C6E730B50D460188CC18944208 +:1027B00000D3A41A4088984200D8FFDF281930BD02 +:1027C0002DE9F041C84D04469046A8780E46A04237 +:1027D00000D8FFDF05EB8607B86A50F8240000B187 +:1027E000FFDFB868002816D0304600F044F90146F3 +:1027F000B868FFF73AFF05000CD0B86A082E40F819 +:10280000245000D3FFDFB9484246294650F826300D +:10281000204698472846BDE8F0812DE9F8431E463A +:102820008C1991460F460546FF2C00D9FFDFB145B4 +:1028300000D9FFDFE4B200954DB300208046E81CCC +:1028400020F00300A84200D0FFDF4946DFF898924D +:10285000684689F8001089F8017089F8024089F803 +:10286000034089F8044089F8054089F8066089F832 +:102870000770414600F008F9002142460F464B46DA +:102880000098C01C20F00300009012B10EE001205F +:10289000D4E703EB8106B062002005E0D6F828C03B +:1028A0004CF82070401CC0B2A042F7D30098491CDD +:1028B00000EB8400C9B200900829E1D3401BBDE8B9 +:1028C000F88310B50446EEF724FD08B1102010BDC2 +:1028D0002078854A618802EB800092780EE0836A56 +:1028E00053F8213043B14A1C6280A180806A50F8BD +:1028F0002100A060002010BD491C89B28A42EED898 +:102900006180052010BD70B505460C460846EEF7FF +:1029100000FD08B1102070BD082D01D3072070BD47 +:1029200025700020608070BD0EB56946FFF7EBFF93 +:1029300000B1FFDF6846FFF7C4FF08B100200EBDFD +:1029400001200EBD10B50446082800D3FFDF6648FD +:10295000005D10BD3EB5054600246946FFF7D3FF74 +:1029600018B1FFDF01E0641CE4B26846FFF7A9FF7D +:102970000028F8D02846FFF7E5FF001BC0B23EBD97 +:1029800059498978814201D9C0B27047FF20704708 +:102990002DE9F041544B062903D007291CD19D791C +:1029A00000E0002500244FF6FF7603EB810713F8C3 +:1029B00001C00AE06319D7F828E09BB25EF823E073 +:1029C000BEF1000F04D0641CA4B2A445F2D8334673 +:1029D00003801846B34201D100201CE7BDE8F04156 +:1029E000EEE6A0F57F43FF3B01D0082901D300208C +:1029F0007047E5E6A0F57F42FF3A0BD0082909D2DF +:102A0000394A9378834205D902EB8101896A51F8EA +:102A100020007047002070472DE9F04104460D4624 +:102A2000A4F57F4143F20200FF3902D0082D01D303 +:102A30000720F0E62C494FF000088A78A242F8D926 +:102A400001EB8506B26A52F82470002FF1D02748B6 +:102A50003946203050F8252020469047B16A284654 +:102A600041F8248000F007F802463946B068FFF7C5 +:102A700027FE0020CFE61D49403131F810004FF607 +:102A8000FC71C01C084070472DE9F843164E88467B +:102A9000054600242868C01C20F00300286020465A +:102AA000FFF7E9FF315D4843B8F1000F01D0002284 +:102AB00000E02A680146009232B100274FEA0D007B +:102AC000FFF7B5FD1FB106E001270020F8E706EB90 +:102AD0008401009A8A602968641C0844E4B2286072 +:102AE000082CD7D3EBE6000008080020445B020066 +:102AF00070B50E461D46114600F0D4F8044629462E +:102B0000304600F0D8F82044001D70BD2DE9F0419A +:102B100090460D4604004FF0000610D00027E01C40 +:102B200020F00300A04200D0FFDFDDB141460020CD +:102B3000FFF77DFD0C3000EB850617B112E0012791 +:102B4000EDE7614F04F10C00A9003C602572606064 +:102B500000EB85002060606812F0B1FD41463868E6 +:102B6000FFF765FD3046BDE8F0812DE9FF4F564C7B +:102B7000804681B020689A46934600B9FFDF2068FE +:102B8000027A424503D9416851F8280020B143F246 +:102B9000020005B0BDE8F08F5146029800F082F8BF +:102BA00086B258460E9900F086F885B27019001D5D +:102BB00087B22068A14639460068FFF756FD040039 +:102BC0001FD0678025802946201D0E9D07465A4646 +:102BD00001230095FFF768F9208831463844012326 +:102BE000029ACDF800A0FFF75FF92088C119384696 +:102BF000FFF78AF9D9F800004168002041F8284021 +:102C0000C7E70420C5E770B52F4C0546206800B91A +:102C1000FFDF2068017AA9420ED9426852F82510D8 +:102C200051B1002342F825304A880068FFF748FD7B +:102C3000216800200A7A08E043F2020070BD4B6868 +:102C400053F8203033B9401CC0B28242F7D808682C +:102C5000FFF700FD002070BD70B51B4E0546002437 +:102C6000306800B9FFDF3068017AA94204D94068B2 +:102C700050F8250000B1041D204670BD70B5124EFD +:102C800005460024306800B9FFDF3068017AA942A8 +:102C900006D9406850F8251011B131F8040B4418DA +:102CA000204670BD10B50A460121FFF7F6F8C01C9A +:102CB00020F0030010BD10B50A460121FFF7EDF822 +:102CC000C01C20F0030010BD8000002070B5044639 +:102CD000C2F11005281912F051FC15F0FF0108D0BF +:102CE000491EC9B2802060542046BDE8704012F0F1 +:102CF000C4BC70BD30B505E05B1EDBB2CC5CD55CFE +:102D00006C40C454002BF7D130BD10B5002409E04D +:102D10000B78521E44EA430300F8013B11F8013BD3 +:102D2000D2B2DC09002AF3D110BD2DE9F04389B0FD +:102D30001E46DDE9107990460D00044622D0024679 +:102D40000846F949FDF7BAFC102221463846FFF73C +:102D5000DCFFE07B000606D5F34A3946102310322B +:102D60000846FFF7C7FF102239464846FFF7CDFF58 +:102D7000F87B000606D5EC4A494610231032084677 +:102D8000FFF7B8FF1021204612F077FC0DE0103E4F +:102D9000B6B208EB0601102322466846FFF7AAFFE9 +:102DA000224628466946FDF789FC102EEFD818D038 +:102DB000F2B241466846FFF789FF10234A4669464A +:102DC00004A8FFF797FF1023224604A96846FFF7DF +:102DD00091FF224628466946FDF770FC09B0BDE820 +:102DE000F08310233A464146EAE770B59CB01E4690 +:102DF0000546134620980C468DF8080020221946F7 +:102E00000DF1090012F0BAFB202221460DF1290034 +:102E100012F0B4FB17A913A8CDE90001412302AABF +:102E200031462846FFF781FF1CB070BD2DE9FF4FEA +:102E30009FB014AEDDE92D5410AFBB49CDE900764B +:102E4000202320311AA8FFF770FF4FF000088DF8FB +:102E500008804FF001098DF8099054F8010FCDF862 +:102E60000A00A088ADF80E0014F8010C1022C0F37F +:102E700040008DF8100055F8010FCDF81100A8881A +:102E8000ADF8150015F8010C2C99C0F340008DF831 +:102E9000170006A8824612F071FB0AA8834610228A +:102EA000229912F06BFBA0483523083802AA40682B +:102EB0008DF83C80CDE900760E901AA91F98FFF797 +:102EC00034FF8DF808808DF809902068CDF80A004D +:102ED000A088ADF80E0014F8010C1022C0F34000D9 +:102EE0008DF810002868CDF81100A888ADF81500FD +:102EF00015F8010C2C99C0F340008DF817005046CE +:102F000012F03CFB58461022229912F037FB8648FB +:102F10003523083802AA40688DF83C90CDE9007648 +:102F20000E901AA92098FFF700FF23B0BDE8F08F9C +:102F3000F0B59BB00C460546DDE922101E4617464B +:102F4000DDE92032D0F801C0CDF808C0B0F805C0E6 +:102F5000ADF80CC00078C0F340008DF80E00D1F839 +:102F60000100CDF80F00B1F80500ADF813000878A6 +:102F70001946C0F340008DF815001088ADF8160012 +:102F800090788DF818000DF11900102212F0F6FA61 +:102F90000DF129001022314612F0F0FA0DF139003E +:102FA0001022394612F0EAFA17A913A8CDE9000158 +:102FB000412302AA21462846FFF7B7FE1BB0F0BD09 +:102FC000F0B5A3B017460D4604461E46102202A8CF +:102FD000289912F0D3FA06A82022394612F0CEFA28 +:102FE0000EA82022294612F0C9FA1EA91AA8CDE976 +:102FF0000001502302AA314616A8FFF796FE169844 +:10300000206023B0F0BDF0B589B00446DDE90E07BD +:103010000D463978109EC1F340018DF800103178CB +:103020009446C1F340018DF801101968CDF80210E3 +:103030009988ADF8061099798DF808100168CDF8D7 +:1030400009108188ADF80D1080798DF80F001023DC +:103050006A46614604A8FFF74DFE2246284604A9A9 +:10306000FDF72CFBD6F801000090B6F80500ADF88E +:103070000400D7F80100CDF80600B7F80500ADF858 +:103080000A000020039010236A46214604A8FFF797 +:1030900031FE2246284604A9FDF710FB09B0F0BD19 +:1030A0001FB51C6800945B68019313680293526813 +:1030B0000392024608466946FDF700FB1FBD10B5A6 +:1030C00088B004461068049050680590002006906F +:1030D000079008466A4604A9FDF7F0FABDF800001B +:1030E000208008B010BD1FB51288ADF800201A88E6 +:1030F000ADF8022000220192029203920246084695 +:103100006946FDF7DBFA1FBD7FB5074B1446054640 +:10311000083B9A1C6846FFF7E6FF224669462846A8 +:10312000FFF7CDFF7FBD00009C5B020070B5044639 +:1031300000780E46012813D0052802D0092813D1A3 +:103140000EE0A06861690578042003F075F9052D8B +:103150000AD0782300220420616903F0C3F803E059 +:103160000420616903F068F931462046BDE87040EB +:1031700001F084B810B500F12D03C2799C78411D8F +:10318000144064F30102C271D2070DD04A795C7910 +:1031900022404A710A791B791A400A718278C978EB +:1031A0008A4200D9817010BD00224A71F5E741784A +:1031B000012900D00C21017070472DE9F04F93B028 +:1031C0004FF0000B0C690D468DF820B009780126F0 +:1031D0000C2017464FF00D084FF0110A4FF0080968 +:1031E0001B2975D2DFE811F01B00C20205031D0385 +:1031F0005C036F03A103B603F70318046004920491 +:103200009F04EB042905330551055C05ED053006E7 +:10321000330662067E06F8061C07E506EA0614B1C8 +:1032200020781D282AD0D5F808805FEA08004FD002 +:1032300001208DF82000686A02220D908DF824206C +:103240000A208DF82500A8690A90A8880028EED0E9 +:1032500098F8001091B10F2910D27DD2DFE801F06B +:103260007C1349DEFCFBFAF9F8F738089CF6F50008 +:1032700002282DD124B120780C2801D00026EEE3BD +:103280008DF82020CAE10420696A03F0D5F8A888E7 +:103290000728EED1204600F0ECFF022809D0204696 +:1032A00000F0E7FF032807D9204600F0E2FF0728D7 +:1032B00002D20120207004E0002CB8D02078012830 +:1032C000D7D198F80400C11F0A2902D30A2061E06F +:1032D000C3E1A070D8F80010E162B8F804102186AC +:1032E00098F8060084F8320001202870032020702E +:1032F00044E00728BDD1002C99D020780D28B8D102 +:1033000098F8031094F82F20C1F3C000C2F3C00254 +:10331000104201D0062000E00720890707D198F865 +:1033200005100142D2D198F806100142CED194F88E +:10333000312098F8051020EA02021142C6D194F813 +:10334000322098F8061090430142BFD198F804004B +:10335000C11F0A29BAD200E006E2617D81427CD811 +:10336000D8F800106160B8F80410218198F80600C0 +:10337000A072012028700E20207003208DF82000FC +:10338000686A0D9004F12D000990601D0A900F30BD +:103390000B9021E12875FDE3412891D1204600F0F2 +:1033A00068FF042802D1E078C00704D1204600F06D +:1033B00060FF0F2884D1A88CD5F80C8080B24FF024 +:1033C000400BE669FFF748FC324641465B464E46F5 +:1033D000CDF80090FFF733F80B208DF82000686AD5 +:1033E0000D90E0690990002108A8FFF79FFE207862 +:1033F000042806D0A07D58B1012809D003280AD09E +:1034000048E305202070032028708DF82060CCE16F +:1034100084F800A032E712202070E8E11128BCD126 +:10342000204600F026FF042802D1E078C00719D01A +:10343000204600F01EFF062805D1E078C00711D114 +:10344000A07D02280ED0204608E0CBE084E070E1A9 +:103450004FE122E102E1E8E019E0AEE100F009FF0E +:1034600011289AD1102208F1010104F13C0012F058 +:1034700085F8607801286ED012202070E078C007AF +:1034800060D0A07D0028C8D00128C6D05AE01128FD +:1034900090D1204600F0EDFE082804D0204600F030 +:1034A000E8FE132886D104F16C00102208F1010116 +:1034B000064612F063F8207808280DD014202070FA +:1034C000E178C8070DD0A07D02280AD06278022AD0 +:1034D00004D00328A1D035E00920F0E708B1012885 +:1034E00037D1C80713D0A07D02281DD0002000903E +:1034F000D4E9062133460EA8FFF777FC10220EA967 +:1035000004F13C0012F00EF8C8B1042042E7D4E9FF +:103510000912201D8DE8070004F12C0332460EA885 +:10352000616BFFF770FDE9E7606BC1F34401491E71 +:103530000068C84000F0010040F08000D7E7207824 +:10354000092806D185F800908DF8209032E3287084 +:10355000EBE30920FBE79CE1112899D1204600F01C +:1035600088FE0A2802D1E078C00704D1204600F086 +:1035700080FE15288CD104F13C00102208F10101D5 +:10358000064611F0FBFF20780A2816D0162020707E +:10359000D4E90932606B611D8DE80F0004F15C0312 +:1035A00004F16C0247310EA8FFF7C2FC10220EA9ED +:1035B000304611F0B7FF18B1F6E20B20207071E22F +:1035C0002046FFF7D7FDA078216A0A18C0F1100144 +:1035D000104612F052F823E3394608A8FFF7A6FD7B +:1035E00006463BE20228B8D1204600F042FE0428FD +:1035F00004D3204600F03DFE082809D3204600F001 +:1036000038FE0E2829D3204600F033FE122824D29B +:10361000A07D0228A1D10E208DF82000686A0D90AF +:1036200098F801008DF82400F0E3022895D1204697 +:1036300000F01FFE002810D0204600F01AFE0128DE +:10364000F9D0204600F015FE0C28F4D004208DF8A7 +:10365000240098F801008DF825005EE21128FCD1C5 +:10366000002CFAD020781728F7D16178606A0229F7 +:1036700011D0002101EB4101182606EBC1011022F7 +:10368000405808F1010111F079FF0420696A00F047 +:10369000E3FD2670F2E50121ECE70B28DDD1002CDB +:1036A000DBD020781828D8D16078616A02281CD035 +:1036B0005FF0000000EB4002102000EBC200095850 +:1036C000B8F8010008806078616A02280FD00020F5 +:1036D00000EB4002142000EBC2000958404650F8AD +:1036E000032F0A604068486039E00120E2E70120CA +:1036F000EEE71128B1D1002CAFD020781928ACD139 +:103700006178606A022912D05FF0000101EB41018B +:103710001C2202EBC1011022405808F1010111F0F6 +:103720002DFF0420696A00F097FD1A20B6E0012100 +:10373000ECE7082891D1002C8FD020781A288CD162 +:10374000606A98F80120017862F347010170616AAC +:10375000D8F8022041F8012FB8F80600888004202C +:10376000696A00F079FD8EE2072013E638780128B7 +:1037700094D1182204F11400796811F044FFE07923 +:10378000C10894F82F0001EAD001E07861F300004D +:10379000E070217D002974D12178032909D0C00768 +:1037A00025D0032028708DF82090686A0D90412064 +:1037B00004E3607DA178884201D90620EAE502266B +:1037C0002671E179204621F0E001E171617A21F072 +:1037D000F0016172A17A21F0F001A172FFF7CAFC39 +:1037E0002E708DF82090686A0D900720E6E2042084 +:1037F000ADE6387805289DD18DF82000686A0D90D7 +:10380000B8680A900720ADF824000A988DF830B007 +:103810006168016021898180A17A81710420207012 +:10382000F4E23978052985D18DF82010696A0D9167 +:10383000391D09AE0EC986E80E004121ADF82410ED +:103840008DF830B01070A88CD7F80C8080B240266C +:10385000A769FFF713FA41463A463346C846CDF802 +:103860000090FEF720FE002108A8FFF75FFCE0783B +:1038700020F03E00801CE0702078052802D00F2048 +:103880000CE049E1A07D20B1012802D0032802D03C +:1038900002E10720C0E584F80080EFE42070EDE449 +:1038A000102104F15C0002F0E8FA606BB0BBA07D6F +:1038B00018B1012801D00520FDE006202870F74846 +:1038C0006063A063BEE23878022894D1387908B1E9 +:1038D0002875B3E3A07D022802D0032805D022E09A +:1038E000B8680028F5D060631CE06078012806D035 +:1038F000A07994F82E10012805D0E84806E0A179B7 +:1039000094F82E00F7E7B8680028E2D06063E0780A +:10391000C00701D0012902D0E04803E003E0F868C5 +:103920000028D6D0A063062011E68DF82090696AA1 +:103930000D91E1784846C90709D06178022903D181 +:10394000A17D29B1012903D0A17D032900D0072041 +:10395000287031E138780528BBD1207807281ED09F +:1039600084F800A005208DF82000686A0D90B868E2 +:103970000A90ADF824A08DF830B003210170E178F1 +:10398000CA070FD0A27D022A1AD000210091D4E9E3 +:10399000061204F15C03401CFFF727FA67E384F882 +:1039A0000090DFE7D4E90923211D8DE80E0004F122 +:1039B0002C0304F15C02401C616BFFF724FB56E30F +:1039C000626BC1F34401491E1268CA4002F0010152 +:1039D00041F08001DAE738780528BDD18DF8200064 +:1039E000686A0D90B8680A90ADF824A08DF830B0E0 +:1039F000042100F8011B102204F15C0111F0BEFD4E +:103A0000002108A8FFF792FB2078092801D0132095 +:103A100044E70A2020709CE5E078C10742D0A17DF0 +:103A2000012902D0022927D038E0617808A80129AD +:103A300016D004F16C010091D4E9061204F15C0384 +:103A4000001DFFF7BDFA0A20287003268DF820809C +:103A5000686A0D90002108A8FFF768FBDDE2C3E269 +:103A600004F15C010091D4E9062104F16C03001D0E +:103A7000FFF7A6FA0026E9E7C0F3440114290DD2A6 +:103A80004FF0006101EBB0104FEAB060E070607879 +:103A9000012801D01020BFE40620FFE6607801284D +:103AA0003FF4B8AC0A2052E5E178C90708D0A17DFF +:103AB000012903D10B20287004202FE028702DE06D +:103AC0000E2028706078616B012817D004F15C0328 +:103AD00004F16C020EA8FFF7E3FA2046FFF74AFB59 +:103AE000A0780EAEC0F11001304411F0C6FD0620E2 +:103AF0008DF82000686A09960D909AE004F16C0335 +:103B000004F15C020EA8FFF7CBFAE9E73978022945 +:103B100003D139790029D1D029758FE28DF82000A1 +:103B2000686A0D9058E538780728F6D1D4E909215C +:103B30006078012809D000BF04F16C00CDE90002D3 +:103B4000029105D104F16C0304E004F15C00F5E797 +:103B500004F15C0304F14C007A680646216AFFF721 +:103B600065F96078012821D1A078216A0A18C0F18E +:103B70001001104611F081FDD4E90923606B04F1B6 +:103B80002D018DE80F0004F15C0304F16C02314655 +:103B90000EA800E054E2FFF7CBF910220EA904F1C1 +:103BA0003C0011F0BFFC08B10B20AFE485F80080A9 +:103BB0008DF82090686A0D908DF824A00CE5387877 +:103BC0000528AAD18DF82000686A0D90B8680A907F +:103BD000ADF824A08DF830B080F80080617801291C +:103BE0001AD0D4E9093204F12D01A66B0392009694 +:103BF000CDE9011304F16C0304F15C0204F14C0102 +:103C0000401CFFF795F9002108A8FFF78FFA6078AC +:103C1000012805D0152041E6D4E90923611DE4E718 +:103C20000E20287006208DF82000686ACDF824B098 +:103C30000D90A0788DF82800CEE438780328C0D104 +:103C4000E079C00770D00F202870072066E7387829 +:103C500004286BD11422391D04F1140011F0D3FC97 +:103C6000616A208CA1F80900616AA078C871E179C5 +:103C7000626A01F003011172616A627A0A73616A11 +:103C8000A07A81F82400162061E485F800A08DF860 +:103C90002090696A50460D9190E000009C5B020004 +:103CA0003878052842D1B868A8616178606A02292D +:103CB00001D0012100E0002101EB4101142606EBB7 +:103CC000C1014058082102F0D8F86178606A0229E1 +:103CD00001D0012100E0002101EB410106EBC1010F +:103CE000425802A8E169FFF70FFA6078626A022879 +:103CF00001D0012000E0002000EB4001102000EB8B +:103D0000C1000223105802A90932FEF7F3FF626ACC +:103D1000FD4B0EA80932A169FFF7E5F96178606AE9 +:103D2000022904D0012103E042E18BE0BDE0002143 +:103D300001EB4101182606EBC101A27840580EA9FB +:103D400011F01CFC6178606A022901D0012100E0B9 +:103D5000002101EB410106EBC1014058A178084464 +:103D6000C1F1100111F089FC05208DF82000686A6E +:103D70000D90A8690A90ADF824A08DF830B0062106 +:103D800001706278616A022A01D0012200E00022FB +:103D900002EB420206EBC202401C8958102211F0CD +:103DA000EDFB002108A8FFF7C1F91220C5F818B0F3 +:103DB00028708DF82090686A0D900B208DF82400F3 +:103DC0000AE43878052870D18DF82000686A0D90D3 +:103DD000B8680A900B20ADF824000A9807210170FA +:103DE0006178626A022901D0012100E0002101EB23 +:103DF0004103102101EBC30151580988A0F80110BB +:103E00006178626A022902D0012101E02FE10021DC +:103E100001EB4103142101EBC30151580A6840F83A +:103E2000032F4968416059E01920287001208DF85E +:103E3000300077E6162028708DF830B0002108A8F1 +:103E4000FFF774F9032617E114202870B0E63878DC +:103E500005282AD18DF82000686A0D90B8680A906C +:103E6000ADF824A08DF830B080F800906278616AD7 +:103E70004E46022A01D0012200E0002202EB42025B +:103E80001C2303EBC202401C8958102211F076FB60 +:103E9000002108A8FFF74AF9152028708DF8206046 +:103EA000686A0D908DF824603CE680E0387805283B +:103EB0007DD18DF82000686A0D90B8680A90ADF841 +:103EC000249009210170616909784908417061698C +:103ED00051F8012FC0F802208988C18020781C2861 +:103EE000A8D1A1E7E078C00702D04FF0060C01E0AE +:103EF0004FF0070C607802280AD000BF4FF0000096 +:103F000000EB040101F1090105D04FF0010004E0CC +:103F10004FF00100F4E74FF000000B78204413EA63 +:103F20000C030B7010F8092F02EA0C02027004D186 +:103F30004FF01B0C84F800C0D2B394F801C0BCF160 +:103F4000010F00D09BB990F800C0E0465FEACC7C3E +:103F500004D028F001060670102606E05FEA887C8F +:103F600005D528F00206067013262E70032694F855 +:103F700001C0BCF1020F00D092B991F800C05FEA15 +:103F8000CC7804D02CF001060E70172106E05FEA11 +:103F90008C7805D52CF002060E70192121700026B0 +:103FA0000078D0BBCAB3C3BB1C20207035E012E040 +:103FB00002E03878062841D11A2019E42078012837 +:103FC0003CD00C283AD02046FFF7F1F809208DF8B4 +:103FD0002000686A0D9031E03878052805D0062069 +:103FE000387003261820287046E005218DF820102F +:103FF000686A0D90B8680A900220ADF8240001208C +:104000008DF830000A980170297D4170394608A862 +:10401000FFF78CF8064618202870012E0ED02BE0F2 +:1040200001208DF82000686A0D9003208DF824008F +:10403000287D8DF8250085F814B012E0287D80B128 +:104040001D202070172028708DF82090686A0D9030 +:1040500002208DF82400394608A8FFF767F80646C5 +:104060000AE00CB1FE2020709DF8200020B1002154 +:1040700008A8FFF75BF810E413B03046BDE8F08FF6 +:104080002DE9F04387B00C464E6900218DF80410ED +:1040900001202578034602274FF007094FF0050C51 +:1040A00085B1012D53D0022D39D1FE2030708DF80D +:1040B0000030606A059003208DF80400207E8DF8A2 +:1040C000050063E02179012925D002292DD003299B +:1040D00028D0042923D1B17D022920D131780D1FA8 +:1040E000042D04D30A3D032D01D31D2917D12189A5 +:1040F000022914D38DF80470237020899DF80410D0 +:1041000088421BD2082001E0945B02008DF8000079 +:10411000606A059057E070780128EBD0052007B061 +:10412000BDE8F0831D203070E4E771780229F5D1F5 +:1041300031780C29F3D18DF80490DDE7083402F8CA +:1041400004CB94E80B0082E80B000320E7E7157826 +:10415000052DE4D18DF800C0656A05959568029536 +:104160008DF8101094F80480B8F1010F13D0B8F155 +:10417000020F2DD0B8F1030F1CD0B8F1040FCED12F +:10418000ADF804700E202870207E6870002168460B +:10419000FEF7CCFF0CE0ADF804700B202870207EF9 +:1041A000002100F01F0068706846FEF7BFFF3770FF +:1041B0000020B4E7ADF804708DF8103085F800C029 +:1041C000207E6870277011466846FEF7AFFFA6E7AD +:1041D000ADF804902B70207F6870607F00F00100C4 +:1041E000A870A07F00F01F00E870E27F2A71C0076E +:1041F0001CD094F8200000F00700687194F82100AA +:1042000000F00700A87100216846FEF78FFF2868BC +:10421000F062A8883086A87986F83200A0694078D4 +:1042200070752879B0700D203070C1E7A97169717F +:10423000E9E700B587B004280CD101208DF8000013 +:104240008DF80400002005918DF8050001466846B0 +:10425000FEF76CFF07B000BD70B50C46054602F0D6 +:10426000EBF821462846BDE870407823002202F092 +:1042700039B808B1007870470C20704770B50C0051 +:1042800005784FF000010CD021702146F0F7D9FFDE +:1042900069482178405D884201D1032070BD022029 +:1042A00070BDF0F7CEFF002070BD0279012A05D065 +:1042B00000220A704B78012B02D003E004207047E3 +:1042C0000A758A6102799300521C0271C150032061 +:1042D0007047F0B587B00F4605460124287905EBF5 +:1042E000800050F8046C7078411E02290AD25249AD +:1042F0003A46083901EB8000314650F8043C284624 +:10430000984704460CB1012C11D12879401E10F0B9 +:10431000FF00287101D00324E0E70A208DF8000097 +:10432000706A0590002101966846FFF7A7FF032CED +:10433000D4D007B02046F0BD70B515460A460446F5 +:1043400029461046FFF7C5FF064674B12078FE28BF +:104350000BD1207C30B100202870294604F10C00DC +:10436000FFF7B7FF2046FEF722FF304670BD7047CB +:1043700070B50E4604467C2111F0A1F90225012EEC +:1043800003D0022E04D0052070BD0120607000E033 +:1043900065702046FEF70BFFA575002070BD28B1A3 +:1043A000027C1AB10A4600F10C01C5E701207047F2 +:1043B00010B5044686B0042002F03EF82078FE28AE +:1043C00006D000208DF8000069462046FFF7E7FF81 +:1043D00006B010BD7CB50E4600218DF80410417862 +:1043E000012903D0022903D0002405E0046900E07C +:1043F00044690CB1217C89B16D4601462846FFF71E +:1044000054FF032809D1324629462046FFF794FF7E +:104410009DF80410002900D004207CBD04F10C0597 +:10442000EBE730B40C460146034A204630BC034B50 +:104430000C3AFEF758BE0000D85B0200945B020005 +:1044400070B50D46040011D085B12101284611F048 +:1044500014F910225449284611F090F852480121CD +:104460000838018044804560002070BD012070BD87 +:1044700070B54D4E00240546083E10E07068AA7BDA +:1044800000EB0410817B914208D1C17BEA7B914211 +:1044900004D10C22294611F045F830B1641C308853 +:1044A0008442EBDB4FF0FF3070BD204670BD70B52D +:1044B0000D46060006D02DB1FFF7DAFF002803DB1A +:1044C000401C14E0102070BD374C083C20886288E6 +:1044D000411C914201D9042070BD6168102201EB9A +:1044E0000010314611F04AF82088401C20802870C6 +:1044F000002070BD2C480838008870472A490839C8 +:104500000888012802D0401E08800020704770B53E +:1045100014460D0018D0BCB10021A170022802D0B1 +:10452000102811D105E0288870B10121A1701080F8 +:1045300008E02846FFF79CFF002805DB401CA07020 +:10454000A8892080002070BD012070BD70B505468F +:1045500014460E000BD000203070A878012808D037 +:1045600005D91149A1F108010A8890420AD9012010 +:1045700070BD24B1287820702888000A507002206D +:1045800008700FE064B14968102201EB0011204669 +:10459000103910F0F3FF287820732888000A607320 +:1045A00010203070002070BD8C0000202DE9F041FB +:1045B00090460C4607460025FE48072F00EB88165C +:1045C00007D2DFE807F00707070704040400012506 +:1045D00000E0FFDF06F81470002D13D0F54880309E +:1045E00000EB880191F82700202803D006EB40005B +:1045F000447001E081F8264006EB44022020507010 +:1046000081F82740BDE8F081F0B51F4614460E46FC +:104610000546202A00D1FFDFE649E648803100EB5D +:10462000871C0CEB440001EB8702202E07D00CEB1B +:10463000460140784B784870184620210AE092F8ED +:104640002530407882F82500F6E701460CEB410062 +:1046500005704078A142F8D192F82740202C03D071 +:104660000CEB4404637001E082F826300CEB41044B +:104670002023637082F82710F0BD30B50D46CE4B75 +:1046800044190022181A72EB020100D2FFDFCB4856 +:10469000854200DDFFDFC9484042854200DAFFDF86 +:1046A000C548401C844207DA002C01DB204630BD9F +:1046B000C148401C201830BDBF48C043FAE710B5C0 +:1046C00004460168407ABE4A52F82020114450B195 +:1046D0000220084420F07F40EEF7AFFA94F908106A +:1046E000BDE81040C9E70420F3E72DE9F047B14EDB +:1046F000803696F82D50DFF8BC9206EB850090F8D6 +:10470000264034E009EB85174FF0070817F814002E +:10471000012806D004282ED005282ED0062800D047 +:10472000FFDF01F00AF9014607EB4400427806EB8F +:10473000850080F8262090F82720A24202D120226E +:1047400080F82720084601F003F92A462146012077 +:10475000FFF72CFF9B48414600EB041002682046FF +:10476000904796F82D5006EB850090F82640202CB7 +:10477000C8D1BDE8F087022000E003208046D0E7E2 +:1047800010B58C4C2021803484F8251084F8261034 +:1047900084F82710002084F8280084F82D0084F87D +:1047A0002E10411EA16044F8100B20746074207319 +:1047B0006073A0738449E077207508704870002109 +:1047C0007C4A103C02F81100491CC9B22029F9D3D7 +:1047D0000120EEF722F90020EEF71FF9012084F8FE +:1047E0002200EEF765FB7948EEF777FB764CA41EC6 +:1047F00020707748EEF771FB6070BDE81040EEF76F +:1048000099B810B5EEF7BBF86F4CA41E2078EEF700 +:104810007DFB6078EEF77AFBBDE8104001F0C5B88B +:10482000202070472DE9F34F624C0025803404EBC3 +:10483000810A89B09AF82500202821D0691E0291AA +:104840006049009501EB0017391D03AB07C983E8E8 +:104850000700A18BADF81C10A07F8DF81E009DF8FD +:104860001500A046C8B10226554951F820400399C9 +:10487000A219114421F07F41019184B102210FE07E +:104880000120EEF7CAF80020EEF7C7F8EEF795F82A +:1048900001F08BF884F82F50A7E00426E4E700210C +:1048A0008DF81810022801D0012820D10398011991 +:1048B0000998081A801C9DF81C1020F07F4001B157 +:1048C0000221353181420BD203208DF81500039867 +:1048D000C4F13201401A20F07F40322403900CE0F2 +:1048E00098F8240018B901F0F8F900284DD0322CBE +:1048F00003D214B101F04DF801E001F056F8324A4C +:10490000107820B393465278039B121B00219DF828 +:104910001840994601281BD0032819D05FF00000E9 +:104920008DF81E00002A04DD981A039001208DF8EE +:1049300018009DF81C0000B102210398254A20F0C0 +:104940007F40039003AB099801F03BF810B110E0F1 +:104950000120E5E79DF81D0018B99BF80000032829 +:1049600012D08DF81C50CDF80C908DF818408DF8B1 +:104970001E509DF8180058B1039801238119002298 +:104980001846EEF79DF806E000200BB0BDE8F08F6A +:104990000120EEF742F897F90C200123002001993D +:1049A000EEF78EF8F87BC00701D0EEF772F901211F +:1049B00012E00000500A0020FF7F841E0020A107A3 +:1049C000E85B0200500800209E0000209361010077 +:1049D000EB460100FFFF3F0088F82F108AF82850AF +:1049E00020226946F74810F00EFE0120CDE72DE9A0 +:1049F000F05FDFF8D083064608EB860090F825507C +:104A0000202D1FD0A8F180002C4600EB8617A0F5C2 +:104A10000079DFF8B4B305E0A24607EB4A0044781A +:104A2000202C0AD0EEF797F809EB04135A4601211F +:104A30001B1D00F0C6FF0028EED0AC4202D033466A +:104A400052461EE0E14808B1AFF30080EEF783F86C +:104A500098F82F206AB1D8F80C20411C891A090255 +:104A6000CA1701EB12610912002902DD0020BDE81E +:104A7000F09F3146FFF7D6FE08B10120F7E7334635 +:104A80002A4620210420FFF7BFFDEFE72DE9F04182 +:104A9000CC4C2569EEF75FF8401B0002C11700EB14 +:104AA0001160001200D4FFDF94F8220000B1FFDF94 +:104AB000012784F8227094F82E00202800D1FFDF0F +:104AC00094F82E60202084F82E00002584F82F50C2 +:104AD00084F8205084F82150BD48256000780228D1 +:104AE00033D0032831D000202077A068401C05D0A7 +:104AF0004FF0FF30A0600120EDF78FFF0020EDF7B1 +:104B00008CFFEEF788F8EEF780F8EDF756FF0FF020 +:104B100085FFB048056005604FF0E0214FF400408C +:104B2000B846C1F88002EEF722F994F82D703846A5 +:104B3000FFF75DFF0028FAD0A248803800EB87100D +:104B400010F81600022802D006E00120CCE73A4611 +:104B500031460620FFF72AFD84F8238004EB870006 +:104B600090F82600202804D09948801E4078EEF75F +:104B7000D3F9207F002803D0EEF73DF8257765773D +:104B800040E5904910B591F82D200024803901EBC3 +:104B9000821100BF11F814302BB1641CE4B2202C38 +:104BA000F8D3202010BD8C4901EB041108600020CF +:104BB000C87321460120FFF7F9FC204610BD10B54F +:104BC000012801D0032800D171B37E4A92F82D301C +:104BD0007C4C0022803C04EB831300BF13F812408E +:104BE0000CB1082010BD521CD2B2202AF6D3784A4C +:104BF00048B1022807D0072916D2DFE801F01506D0 +:104C0000080A0C0E100000210AE01B2108E03A21DE +:104C100006E0582104E0772102E0962100E0B5216A +:104C200051701070002010BD072010BD684810B5ED +:104C30004078EEF702F880B210BD10B5202811D2EE +:104C4000604991F82D30A1F1800202EB831414F831 +:104C500010303BB191F82D3002EB831212F8102086 +:104C6000012A01D0002010BD91F82D20014600201E +:104C7000FFF79CFC012010BD10B5EDF76CFFBDE8FF +:104C80001040EDF7DABF2DE9F0410E464D4F0178A7 +:104C90002025803F0C4607EB831303E0254603EBFA +:104CA00045046478944202D0202CF7D108E0202CEF +:104CB00006D0A14206D103EB41014978017007E01B +:104CC00000209FE403EB440003EB4501407848706B +:104CD000424F7EB127B1002140F2DD30AFF30080BA +:104CE0003078A04206D127B100214FF47870AFF39D +:104CF0000080357027B1002140F2E530AFF300802D +:104D000001207FE410B542680B689A1A1202D4178A +:104D100002EB1462121216D4497A91B1427A82B926 +:104D20002F4A006852F82110126819441044001DDF +:104D3000891C081A0002C11700EB1160001232280A +:104D400001DB012010BD002010BD2DE9F047814698 +:104D50001C48214E00EB8100984690F82540202009 +:104D6000107006F50070154600EB81170BE000BFD0 +:104D700006EB04104946001DFFF7C4FF28B107EBFE +:104D800044002C704478202CF2D1297888F8001047 +:104D900013E000BF06EB0415291D4846FFF7B2FFDC +:104DA00068B988F80040A97B99F80A00814201D8C7 +:104DB0000020DEE407EB44004478202CEAD10120F7 +:104DC000D7E40000D00A0020FFFF3F0000000000F1 +:104DD0009E00002000F50040500800200000000068 +:104DE000E85B02002DE9FC410E4607460024FE4D1B +:104DF00009E000BF9DF8000005EB0010816838460F +:104E000000F0F3FD01246B4601AA31463846FFF756 +:104E10009CFF0028EED02046BDE8FC8170B504461A +:104E2000F2480125A54300EB841100EB85104022D8 +:104E300010F0A4FBEE4E26B1002140F25F40AFF32C +:104E40000080EA48803000EB850100EB8400D0F858 +:104E50002500C1F8250026B1002140F26340AFF3E0 +:104E60000080284670BD2DE9FC418446DF48154688 +:104E7000089C00EB85170E4617F81400012803D094 +:104E8000022801D00020C7E70B46DA4A012160461C +:104E900000F097FDA8B101AB6A4629463046FFF7FE +:104EA00054FF70B1D1489DF804209DF80010803067 +:104EB00000EB85068A4208D02B460520FFF7A4FBAD +:104EC0000BE02A462146042014E0202903D007EBFA +:104ED0004100407801E096F8250007EB4401487056 +:104EE0009DF80000202809D007EB400044702A46B6 +:104EF00021460320FFF75AFB01208DE706F8254FD6 +:104F00000120F070F3E7B84901EB0010001DFFF736 +:104F1000D6BB7CB51D46134604460E4600F108027A +:104F200021461846EDF796FE94F908000F2804DD97 +:104F30001F3820722068401C206096B10220AE49C4 +:104F400051F82610461820686946801B20F07F40E3 +:104F5000206094F908002844C01C1F2803DA0120AF +:104F600009E00420EBE701AAEDF774FE9DF80400C8 +:104F700010B10098401C009000992068314408440A +:104F8000C01C20F07F4060607CBD2DE9FE430C46D4 +:104F900006460978607990722079984615465072D5 +:104FA00041B19248803090F82E1020290AD0006933 +:104FB000401D0BE0D4E90223217903B02846BDE867 +:104FC000F043A6E78D484178701D084420F07F47E4 +:104FD000217900222846A368FFF79BFF394628461F +:104FE00000F003FDD4E9023221796846FFF791FF12 +:104FF00041462846019CFFF7F5FE2B46224600213C +:10500000304600F0DEFC002803D13146284600F08F +:10501000ECFCBDE8FE832DE9FE4F814600F0A1FCCB +:1050200030B1002799F8000020B10020BDE8FE8FC4 +:105030000127F7E76D4D6E4C4FF0000A803524B123 +:10504000002140F2D640AFF3008095F82D8085F81E +:1050500023A0002624B1002140F2DB40AFF3008002 +:105060001FB94046FFF7DAFE804624B1002140F226 +:10507000E340AFF30080EDF76EFD43466A464946D4 +:10508000FFF783FF24B1002140F2E940AFF3008035 +:1050900095F82E0020280CD029690098401A0002AB +:1050A000C21700EB1260001203D5684600F09DFCA9 +:1050B000012624B1002140F2F340AFF3008095F8BF +:1050C00023000028BBD124B1002140F2F940AFF306 +:1050D0000080EDF740FD6B46464A002100F071FC70 +:1050E0000028A3D027B941466846FFF77BFE064358 +:1050F00026B16846FFF7E3FAC9F8080024B1002199 +:1051000040F20C50AFF3008001208FE72DE9F04F03 +:1051100089B08B46824600F024FC344C803428B39E +:105120009BF80000002710B1012800D0FFDF304DB0 +:1051300025B1002140F28250AFF300802A490120BE +:1051400001EB0A18A94607905FEA090604D000217E +:1051500040F28A50AFF30080079800F0F9FB94F812 +:105160002D50002084F8230067B119E094F82E0038 +:105170000127202800D1FFDF9BF800000028D6D0AF +:10518000FFDFD4E72846FFF749FE054626B1002198 +:1051900040F29450AFF3008094F823000028D3D15C +:1051A00026B1002140F29E50AFF30080EDF7D3FC12 +:1051B0002B4602AA59460790FFF7E7FE98F80F0022 +:1051C0005FEA060900F001008DF8130004D0002109 +:1051D0004FF4B560AFF300803B462A4602A9CDF8F4 +:1051E00000A007980CE0000050080020500A0020A2 +:1051F00000000000FFFF3F00E85B02009E0000206F +:10520000FFF731FE064604EB850090F82800009079 +:10521000B9F1000F04D0002140F2AF50AFF300808D +:1052200000F08BFB0790B9F1000F04D0002140F291 +:10523000B550AFF3008094F82300002884D1B9F171 +:10524000000F04D0002140F2BD50AFF300800DF1FB +:10525000080C9CE80E00C8E90112C8F80C304EB3E7 +:105260005FEA090604D0002140F2CA50AFF3008083 +:105270000098B84312D094F82E0020280ED126B101 +:10528000002140F2CF50AFF300802846FFF7AFFB7C +:1052900020B99BF80000D8B3012849D0B9F1000F1C +:1052A00004D0002140F2EC50AFF30080284600F01B +:1052B0003DFB01265FEA090504D0002140F2F550CC +:1052C000AFF30080079800F043FB25B1002140F2C6 +:1052D000F950AFF300808EB194F82D0004EB8000FC +:1052E00090F82600202809D025B100214FF4C06095 +:1052F000AFF30080F9484078EDF70EFE25B10021AC +:1053000040F20560AFF3008009B03046BDE8F08F91 +:10531000FFE7B9F1000F04D0002140F2D750AFF3FE +:10532000008094F82D2051460420FFF73FF9C0E794 +:10533000002E3FF409AF002140F2E250AFF30080AD +:1053400002E72DE9F84FE64D814695F82D004FF024 +:105350000008E44C4FF0010B474624B1002140F215 +:105360001360AFF30080584600F0F2FA85F823701E +:1053700024B100214FF4C360AFF3008095F82D00F5 +:10538000FFF74CFD064695F8230028B1002CE4D029 +:10539000002140F21E604BE024B1002140F2226067 +:1053A000AFF30080CE48803800EB861111F8190069 +:1053B000032856D1334605EB830A4A469AF825005E +:1053C000904201D1012000E0002000900AF1250068 +:1053D0000021FFF758FC01460098014203D001224A +:1053E0008AF82820AF77E1B324B1002140F227608A +:1053F000AFF30080324649460120FFF7D7F89AF80C +:1054000028A024B1002140F23260AFF3008000F008 +:1054100094FA834624B1002140F23760AFF3008054 +:1054200095F8230038B1002C97D0002140F23B6062 +:10543000AFF3008091E7BAF1000F07D095F82E0086 +:10544000202803D13046FFF7D2FAE0B124B1002181 +:1054500040F24F60AFF30080304600F067FA4FF043 +:10546000010824B100214FF4CB60AFF3008058460F +:1054700000F06EFA24B1002140F25C60AFF30080CE +:105480004046BDE8F88F002CF1D0002140F24A6080 +:10549000AFF30080E6E70020EDF798BA0120EDF7C2 +:1054A00095BA8E48007870472DE9F0418C4C94F8FD +:1054B0002E0020281FD194F82D6004EB860797F862 +:1054C0002550202D00D1FFDF8549803901EB861062 +:1054D00000EB4500407807F8250F0120F87084F8AC +:1054E0002300294684F82E50324602202234FFF74A +:1054F0005DF80020207004E42DE9F0417A4E784CEC +:10550000012538B1012821D0022879D003287DD087 +:10551000FFDFF0E700F03DFAFFF7C6FF207E00B1A5 +:10552000FFDF84F821500020EDF777FAA168481CCE +:1055300004D0012300221846EDF7C2FA14F82E0F0A +:10554000217806EB01110A68012154E0FFF7ACFF56 +:105550000120EDF762FA94F8210050B1A068401CD8 +:1055600007D014F82E0F217806EB01110A680621E6 +:1055700041E0207EDFF86481002708F1020801285D +:1055800003D002281ED0FFDFB5E7A777EDF733FB86 +:1055900098F80000032801D165772577607D53498D +:1055A00051F8200094F8201051B948B161680123E6 +:1055B000091A00221846EDF783FA022020769AE7AE +:1055C000277698E784F8205000F0E3F9A07F50B1E7 +:1055D00098F8010061680123091A00221846EDF7C6 +:1055E0006FFA257600E0277614F82E0F217806EB67 +:1055F00001110A680021BDE8F041104700E005E014 +:1056000036480078BDE8F041EDF786BCFFF74CFF67 +:1056100014F82E0F217806EB01110A680521EAE73C +:1056200010B52F4C94F82E00202800D1FFDF14F87D +:105630002E0F21782C4A02EB01110A68BDE81040B8 +:10564000042110477CB5264C054694F82E002028EE +:1056500000D1FFDFA068401C00D0FFDF94F82E00CF +:10566000214901AA01EB0010694690F90C00284479 +:10567000EDF7F0FA9DF904000F2801DD012000E0AC +:105680000020009908446168084420F07F41A1602F +:1056900094F82100002807D002B00123BDE8704033 +:1056A00000221846EDF70CBA7CBD30B5104A0B1A33 +:1056B000541CB3EB940F1FD3451AB5EB940F1BD3B7 +:1056C000934203D9101A43185B1C15E0954211D977 +:1056D000511A0844401C43420EE000009C00002088 +:1056E000D00A00200000000050080020E85B020003 +:1056F000FF7F841EFFDF0023184630BD01230022F8 +:1057000001460220EDF7DCB90220EDF786B9EDF78E +:1057100022BA2DE9FC47BA4C054694F82E00202801 +:1057200000D1FFDF642D58D3B64A0021521B71EB24 +:10573000010052D394F82E20A0462046DFF8C892EC +:1057400090F82D7009EB0214D8F8000001AA284443 +:105750006946EDF77FFA9DF90400002802DD009804 +:10576000401C0090A068009962684618B21A22F0A6 +:105770007F42B2F5800F30D208EB8702444692F8A0 +:105780002520202A0AD009EB02125268101A0002C2 +:10579000C21700EB1260001288421EDBA068401C9A +:1057A00010D0EDF7D8F9A168081A0002C11700EB74 +:1057B00011600012022810DD0120EDF72EF94FF0E4 +:1057C000FF30A06020682844206026F07F402061E0 +:1057D000012084F82300BDE8FC870020FBE72DE9C9 +:1057E000F047874C074694F82D00A4F1800606EB9D +:1057F000801010F8170000B9FFDF94F82D50A04674 +:10580000824C24B100214FF40760AFF3008040F6D2 +:105810007C0940F6850A06EB851600BF16F81700CE +:10582000012818D0042810D005280ED006280CD046 +:105830001CB100214846AFF3008020BF002CEDD002 +:1058400000215046AFF30080E8E72A4639460120A0 +:10585000FEF7ACFEF2E74FF0010A4FF000094546B3 +:1058600024B1002140F68C00AFF30080504600F0D8 +:105870006FF885F8239024B1002140F69100AFF332 +:10588000008095F82D00FFF7C9FA064695F8230029 +:1058900028B1002CE4D0002140F697001FE024B18D +:1058A000002140F69B00AFF3008005EB860000F17D +:1058B000270133463A462630FFF7E5F924B10021A7 +:1058C00040F69F00AFF3008000F037F8824695F86D +:1058D000230038B1002CC3D0002140F6A500AFF35F +:1058E0000080BDE785F82D60012085F82300504633 +:1058F00000F02EF8002C04D0002140F6B200AFF3E7 +:105900000080BDE8F08730B504463D480D4690F86C +:105910002D003B49803901EB801010F8140000B9CC +:10592000FFDF394800EB0410C57330BD344981F8FE +:105930002D00012081F82300704710B5344808B1CC +:10594000AFF30080EFF3108000F0010072B610BDDD +:1059500010B5002804D12F4808B1AFF3008062B61B +:1059600010BD2D480068C005C00D10D0103840B2E1 +:10597000002804DB00F1E02090F8000405E000F0CE +:105980000F0000F1E02090F8140D40097047082046 +:10599000704710B51A4C94F82400002804D1F6F78B +:1059A0005FF8012084F8240010BD10B5144C94F861 +:1059B0002400002804D0F6F77CF8002084F82400A6 +:1059C00010BD10B51C685B68241A181A24F07F44B7 +:1059D00020F07F40A14206D8B4F5800F03D2904258 +:1059E00001D8012010BD002010BDD0E90032D21A2C +:1059F00021F07F43114421F07F41C0E9003170471D +:105A0000D00A0020FF1FA10750080020000000005E +:105A1000000000000000000004ED00E02DE9F0416E +:105A2000044680074FF000054FF001060CD56B4887 +:105A3000056006600EF01BFE20B16948016841F464 +:105A40008061016024F00204E0044FF0FF3705D5C7 +:105A500064484660C0F8087324F48054600003D59D +:105A60006148056024F08044E0050FD55F48C0F828 +:105A70000052C0F808735E490D60091D0D605C4A54 +:105A800004210C321160066124F48074A00409D54D +:105A900058484660C0F80052C0F808735648056080 +:105AA00024F40054C4F38030C4F3C031884200D0E1 +:105AB000FFDF14F4404F14D050484660C0F808731C +:105AC0004F488660C0F80052C0F808734D490D6019 +:105AD0000A1D16608660C0F808730D60166024F415 +:105AE000404420050AD5484846608660C0F80873DF +:105AF000C0F848734548056024F400640EF068FF60 +:105B00004348044200D0FFDFBDE8F081F0B5002239 +:105B1000202501234FEA020420FA02F1C9072DD003 +:105B200051B2002910DB00BF4FEA51174FEA870737 +:105B300001F01F0607F1E02703FA06F6C7F88061B7 +:105B4000BFF34F8FBFF36F8F0CDB00BF4FEA5117CE +:105B50004FEA870701F01F0607F1E02703FA06F670 +:105B6000C7F8806204DB01F1E02181F8004405E020 +:105B700001F00F0101F1E02181F8144D02F1010261 +:105B8000AA42C9D3F0BD10B5224C20600846F6F7F2 +:105B90007CF82068FFF742FF2068FFF7B7FF0EF0A0 +:105BA000FDFA00F01AF90EF013FF0EF056FEEDF7B5 +:105BB0007FF9BDE810400EF0A1BB10B5154C206870 +:105BC000FFF72CFF2068FFF7A1FF0EF001FFF6F7AB +:105BD0004FF90020206010BD0A207047FC1F0040D4 +:105BE0003C17004000C0004004E501400080004038 +:105BF0000485004000D0004004D5004000E0004093 +:105C000000F0004000F5004000B0004008B5004042 +:105C1000FEFF0FFDA000002070B526490A680AB3F8 +:105C20000022154601244B685B1C4B600C2B00D3F3 +:105C30004D600E7904FA06F30E681E420FD0EFF3A2 +:105C4000108212F0010272B600D001220C689C434F +:105C50000C6002B962B649680160002070BD521C38 +:105C60000C2AE0D3052070BD4FF0E0214FF48000F6 +:105C7000C1F800027047EFF3108111F0010F72B606 +:105C80004FF0010202FA00F20A48036842EA0302F6 +:105C9000026000D162B6E7E706480021016041607A +:105CA00070470121814003480068084000D001206E +:105CB00070470000A40000200120810708607047A1 +:105CC0000121880741600021C0F8001118480170C7 +:105CD000704717490120087070474FF08040D0F896 +:105CE0000001012803D012480078002800D00120CC +:105CF000704710480068C00700D0012070470D4869 +:105D00000C300068C00700D00120704709481430EB +:105D100000687047074910310A68D20306D5096840 +:105D200001F00301814201D101207047002070473A +:105D3000AC000020080400400021017008467047B4 +:105D40000146002008707047EFF3108101F0010157 +:105D500072B60278012A01D0012200E0002201235C +:105D6000037001B962B60AB1002070474FF40050C9 +:105D70007047E9E7EFF3108111F0010F72B64FF0B1 +:105D80000002027000D162B600207047F2E7000006 +:105D90002DE9F04115460E460446002700F0E7F8CD +:105DA000A84215D3002341200FE000BF94F8422001 +:105DB000A25CF25494F84210491CB1FBF0F200FBD3 +:105DC00012115B1C84F84210DBB2AB42EED3012708 +:105DD00000F0D9F83846BDE8F081704910B5802050 +:105DE00081F800046E49002081F8420081F84100EA +:105DF000433181F8420081F84100433181F842008B +:105E000081F841006748FFF797FF6648401CFFF79D +:105E100093FFECF7BBFFBDE8104000F0B4B84020A2 +:105E200070475F4800F0A3B80A4601465C48AFE7F8 +:105E3000402070475A48433000F099B80A4601465E +:105E400057484330A4E7402101700020704710B547 +:105E500004465348863000F08AF82070002010BDB8 +:105E60000A4601464E4810B58630FFF791FF08B14B +:105E7000002010BD42F2070010BD70B50C4605466B +:105E8000412900D9FFDF48480068103840B200F0CF +:105E900050F8C6B20D2000F04CF8C0B2864203D2D2 +:105EA000FFDF01E0ECF7C2FF224629463C48FFF73E +:105EB0006FFF0028F6D070BD2DE9F041394F002565 +:105EC00006463F1D57F82540204600F041F810B324 +:105ED0006D1CEDB2032DF5D33148433000F038F896 +:105EE000002825D02E4800F033F8002820D02C4878 +:105EF000863000F02DF800281AD0ECF76DFF294805 +:105F0000FFF722FFB0F5005F00D0FFDFBDE8F041F2 +:105F10002448FFF72FBF94F841004121265414F87C +:105F2000410F401CB0FBF1F201FB12002070D3E7DF +:105F300051E7002804DB00F1E02090F8000405E0C0 +:105F400000F00F0000F1E02090F8140D40097047B8 +:105F500010F8411F4122491CB1FBF2F302FB13115F +:105F60004078814201D1012070470020704710F82D +:105F7000411F4078814201D3081A02E0C0F141007C +:105F80000844C0B2704710B50648FFF7DDFE002890 +:105F900003D1BDE81040ECF70ABF10BD0DE000E0F2 +:105FA000000B0020B000002004ED00E070B5154D9E +:105FB0002878401CC4B26878844202D0F5F7EFFF1D +:105FC0002C7070BD2DE9F0410E4C4FF0E02600BF63 +:105FD000F5F7DAFF20BF40BF20BF677820786070F8 +:105FE000D6F80052EBF70CFA854305D1D6F8040237 +:105FF00010B92078B842EBD0F5F7C1FF0020BDE81A +:10600000F0810000C00000202DE9F04101252803A7 +:106010004FF0E0210026C1F88001BFF34F8FBFF39E +:106020006F8F1F4CC4F800610C2000F02CF81D4845 +:1060300001680268C94341F3001142F01002026096 +:10604000C4F804532560491C00E020BFD4F80021A7 +:10605000002AFAD019B9016821F010010160124834 +:1060600007686560C4F80853C4F800610C2000F0AC +:106070000AF83846BDE8F08110B50446FFF7C4FFC2 +:106080002060002010BD002809DB00F01F02012164 +:1060900091404009800000F1E020C0F88012704774 +:1060A00000C0004010ED00E008C500402DE9F047B9 +:1060B000FF4C0646FF21A06800EB06121170217804 +:1060C000FF2910D04FF0080909EB011109EB061761 +:1060D0004158C05900F0F4F9002807DDA168207884 +:1060E00001EB061108702670BDE8F08794F8008077 +:1060F00045460DE0A06809EB05114158C05900F074 +:10610000DFF9002806DCA068A84600EB0810057837 +:10611000FF2DEFD1A06800EB061100EB08100D7009 +:106120000670E1E7F0B5E24B0446002001259A68CD +:106130000C269B780CE000BF05EB0017D75DA7424B +:1061400004D106EB0017D7598F4204D0401CC0B2CF +:106150008342F1D8FF20F0BD70B5FFF7D8FAD44CD8 +:1061600008252278A16805EB0212895800F0A8F9E9 +:10617000012808DD2178A06805EB01114058BDE831 +:106180007040FFF7BBBAFFF78CF9BDE87040ECF741 +:10619000C3BE2DE9F041C64C2578FFF7B8FAFF2DB4 +:1061A0006ED04FF00808A26808EB0516915900F070 +:1061B00087F90228A06801DD80595DE000EB051138 +:1061C00009782170022101EB0511425C5AB1521E7F +:1061D0004254815901F5800121F07F4181512846C7 +:1061E000FFF764FF34E00423012203EB051302EB05 +:1061F000051250F803C0875CBCF1000F10D0BCF54D +:10620000007F10D9CCF3080250F806C00CEB423CDA +:106210002CF07F4C40F806C0C3589A1A520A09E085 +:10622000FF2181540AE0825902EB4C3222F07F4276 +:106230008251002242542846FFF738FF0C21A06803 +:1062400001EB05114158E06850F827203846904787 +:106250002078FF2814D0FFF75AFA2278A16808EBBB +:1062600002124546895800F02BF9012893DD217868 +:10627000A06805EB01114058BDE8F041FFF73EBAB8 +:10628000BDE8F081F0B51D4614460E460746FF2BCB +:1062900000D3FFDFA00700D0FFDF8548FF210022E9 +:1062A000C0E90247C57006710170427082701046E5 +:1062B000012204E002EB0013401CE154C0B2A842EA +:1062C000F8D3F0BD70B57A4C0646657820798542E2 +:1062D00000D3FFDFE06840F825606078401C607004 +:1062E000284670BD2DE9FF5F1D468B460746FF24FB +:1062F000FFF70DFADFF8B891064699F80100B842A9 +:1063000000D8FFDF00214FF001084FF00C0A99F888 +:106310000220D9F808000EE008EB0113C35CFF2B44 +:106320000ED0BB4205D10AEB011350F803C0DC4587 +:106330000CD0491CC9B28A42EED8FF2C02D00DE025 +:106340000C46F6E799F803108A4203D1FF2004B007 +:10635000BDE8F09F1446521C89F8022008EB041196 +:106360000AEB0412475440F802B00421029B0022B9 +:10637000012B01EB04110CD040F801204FF4007800 +:1063800008234FF0020C454513D9E905C90D02D089 +:1063900002E04550F2E7414606EB413203EB0413BD +:1063A00022F07F42C250691A0CEB0412490A815450 +:1063B0000BE005B9012506EB453103EB041321F091 +:1063C0007F41C1500CEB0411425499F80050204613 +:1063D000FFF76CFE99F80000A84201D0FFF7BCFE61 +:1063E0003846B4E770B50C460546FFF790F9064607 +:1063F00021462846FFF796FE0446FF281AD02C4D6A +:10640000082101EB0411A8684158304600F058F803 +:1064100000F58050C11700EBD14040130221AA685B +:1064200001EB0411515C09B100EB4120002800DCB4 +:10643000012070BD002070BD2DE9F04788468146DF +:10644000FFF770FE0746FF281BD0194D2E78A8686D +:106450003146344605E0BC4206D0264600EB061223 +:106460001478FF2CF7D10CE0FF2C0AD0A6420CD1F7 +:1064700000EB011000782870FF2804D0FFF76CFEB5 +:1064800003E0002030E6FFF73FF941464846FFF7BA +:10649000A9FF0123A968024603EB0413FF20C85497 +:1064A000A878401EB84200D1A87001EB041001E0AA +:1064B000CC0B002001EB061100780870104613E6A3 +:1064C000081A0002C11700EB1160001270470000AB +:1064D0005E4800210170417010218170704770B5D5 +:1064E000054616460C460220ECF7F2F95749012002 +:1064F00008705749F01E086056480560001F046088 +:1065000070BD10B50220ECF7E3F950490120087086 +:1065100051480021C0F80011C0F80411C0F808115A +:106520004E494FF40000086010BD48480178D9B1C9 +:106530004B4A4FF4000111604749D1F80031002265 +:10654000002B1CBFD1F80431002B02D0D1F8081168 +:1065500019B142704FF0100104E04FF00101417099 +:1065600040490968817002704FF00000ECF7B0B943 +:1065700010B50220ECF7ACF934480122002102707A +:106580003548C0F80011C0F80411C0F808110260C5 +:1065900010BD2E480178002904BF407870472E486E +:1065A000D0F80011002904BF02207047D0F8001174 +:1065B00000291CBFD0F80411002905D0D0F808012B +:1065C000002804BF01207047002070471F4800B515 +:1065D0000278214B4078C821491EC9B282B1D3F854 +:1065E00000C1BCF1000F10D0D3F8000100281CBF7F +:1065F000D3F8040100280BD0D3F8080150B107E00C +:10660000022802D0012805D002E00029E4D1FFDFF2 +:10661000002000BD012000BD0C480178002904BF06 +:10662000807870470C48D0F8001100291CBFD0F8C2 +:106630000411002902D0D0F8080110B14FF0100069 +:10664000704708480068C0B270470000C2000020D0 +:1066500010F5004008F5004000F0004004F501404E +:1066600008F5014000F400405648002101704170D7 +:10667000704770B5064614460D460120ECF728F920 +:1066800051480660001D0460001D05604F49002050 +:10669000C1F850014E49032008604F494D48086039 +:1066A000091D4E48086070BD2DE9F041054645487A +:1066B0000C46012606704A4945EA024040F08070C7 +:1066C00008600DF0AAFF002804BF464804600027B8 +:1066D000454CC4F80471464944480860002D02BF87 +:1066E000C4F800622660BDE8F081012D18BFFFDF0D +:1066F000C4F80072266040493E480860BDE8F08159 +:106700003048017871B13A4A384911603649D1F8B8 +:1067100004210021002A08BF417002D0374A1268C4 +:10672000427001700020ECF7D3B8264801780029A8 +:1067300004BF407870472C48D0F80401002808BFF7 +:1067400070472E480068C0B27047002808BF7047E5 +:1067500030B51C480078002808BFFFDF2248D0F879 +:106760000411002918BF30BD0224C0F80443DFF82B +:1067700090C0DCF80010C1F30015DCF8001041F007 +:106780001001CCF80010D0F80411002904BF4FF418 +:1067900000414FF0E02206D1C2F8801220BFD0F8AD +:1067A0000431002BF8D02DB9DCF8001021F01001D5 +:1067B000CCF80010C0F8084330BD0B4901208860B8 +:1067C00070470000C500002008F5004000100040A0 +:1067D0001CF500405011004098F501400CF00040BD +:1067E00004F5004018F5004000F0004000000203EE +:1067F00008F501400000020204F5014000F40040E9 +:1068000010ED00E010B5FE48002401214470047032 +:1068100044728472C17280F82540C462846380F837 +:106820003C4080F83D40FF2180F83E105F2180F819 +:106830003F1018300FF052F8F249601E0860091D31 +:106840000860091D0C60091D0860091D0C60091D08 +:106850000860091D0860091D0860091D0860091D00 +:106860000860091D0860091D0860091D0860091DF0 +:10687000086010BDE448016801F00F01032904BF5E +:1068800001207047016801F00F01042904BF0220B4 +:106890007047016801F00F01052904D0006800F07D +:1068A0000F00062807D1D948006810F0060F0CBF6A +:1068B00008200420704700B5FFDF012000BD10B59F +:1068C000CF4C0168A1614168E161007A84F8200041 +:1068D000207E48B1207FF7F7C4FCA07E011C18BFC2 +:1068E0000121207FF7F7ACFC607E002808BF10BDB7 +:1068F000607FF7F7B6FCE07E011C18BF0121607FC6 +:10690000BDE81040F7F79CBC30B5002405460129CE +:106910000AD0022908BF4FF0807405D0042916BFA1 +:1069200008294FF0C744FFDF44F4847040F480101E +:10693000B749086045F4403001F1040140F00070AF +:10694000086030BD30B50024054601290AD002296F +:1069500008BF4FF0807405D0042916BF08294FF0F6 +:10696000C744FFDF44F4847040F48010A8490860F5 +:1069700045F4403001F1040140F000700860A54882 +:10698000D0F80001002818BFFFDF30BD2DE9F0412D +:1069900002274FF0E02801250024C8F88071BFF3DA +:1069A0004F8FBFF36F8F9C48046005600DF05FFE52 +:1069B0009A4E18B1306840F4806030600DF02DFEC2 +:1069C00038B1306820F0770040F0880040F0004097 +:1069D00030609449924808604FF01020806CB0F10C +:1069E000FF3F04D090490A6860F317420A608F495C +:1069F00040F25B600860091F40F203100860081F46 +:106A00000560814903200860894805608A4A8949F0 +:106A100011608B4A89491160121F8A49116001680F +:106A200021F440710160016841F480710160C8F88F +:106A3000807278491020C1F80403714880F8314011 +:106A4000C462BDE8F0816E4A0368C2F802308088F3 +:106A5000D080117270476A4B10B51A7A8A4208D1F9 +:106A600001460622981C0EF05DFD002804BF01209F +:106A700010BD002010BD624890F825007047604AA4 +:106A8000517010707047F0B50546800000F18040ED +:106A900000F580508B88C0F820360B78D1F80110B3 +:106AA00043EA0121C0F8001605F10800012707FAA2 +:106AB00000F6654C002A04BF2068B04304D0012AC8 +:106AC00018BFFFDF206830432060206807FA05F117 +:106AD00008432060F0BD0EF0D1B8494890F832006C +:106AE00070475A4AC178116000685949000208602D +:106AF0007047252808BF02210ED0262808BF1A217A +:106B00000AD0272808BF502106D00A2894BF0422A3 +:106B1000062202EB4001C9B24E4A11604E4908609C +:106B2000704737498A7A012A49D0022A18BF70472C +:106B30004B7E002B08BF7047012A44D0CB7E4A7F92 +:106B400013F1000C18BF4FF0010C24231844434BE1 +:106B50001860434B0020C3F84C0110028CF0010276 +:106B600040EA025040F0031291F82000830003F144 +:106B7000804303F5C043C3F810253A4A8B7F02EBEC +:106B80008000DA0002F1804202F5F832C2F8140502 +:106B9000DFF8D4C0C2F810C5C97FCA0002F1804234 +:106BA00002F5F832C2F814052648C2F81005012093 +:106BB00000FA03F288402D491043086070470B7EAD +:106BC000002BB9D170478B7E0A7F002B14BF4FF08A +:106BD000010C4FF0000C1123B8E72DE9F0410D4EE8 +:106BE000804603200D46C6F8000220492048086070 +:106BF00028460EF082F80124014FB8F1000F39E069 +:106C0000DC0B0020000E0040101500401414004062 +:106C10001415004000100040FC1F00403C170040CD +:106C20002C000089781700408C1500403815004072 +:106C30005016004000000E0408F50140408000405E +:106C4000A4F50140101100404016004024150040FA +:106C50001C15004008150040541500404C850040AC +:106C600000800040006000404C81004004F501407D +:106C70000000040404BFBC72346026D0B8F1010FD8 +:106C800023D1FE48006860B915F00C0F09D0C6F892 +:106C90000443012000F0B4FEF463346487F83C4000 +:106CA00002E0002000F0ACFE28460EF00EF90220B3 +:106CB000B8720DF0CAFC38B90DF0D9FC20B9F04813 +:106CC000016841F4C02101607460EE48C464EE487C +:106CD00000682946BDE8F04123E72DE9F047EB4E77 +:106CE000814603200D46C6F80002DFF8A883E84875 +:106CF000C8F8000008460EF000F828460EF0E5F847 +:106D00000124E54FB9F1000F03D0B9F1010F0AD00A +:106D100026E0BC72B86B40F48010B8634FF480106A +:106D2000C8F800001CE00220B872B86B40F40010F4 +:106D3000B8634FF40010C8F80000D048006860B98C +:106D400015F00C0F09D0C6F80443012000F058FEDE +:106D5000F463346487F83C4002E0002000F050FE09 +:106D6000EBF794FF2946BDE8F047DAE62DE9F84F46 +:106D7000C64C8246032088461746C4F80002DFF856 +:106D80001493C348C9F8000010460DF0B6FFDFF8B1 +:106D90000CB3C14E0125BAF1000F04BFCBF800407F +:106DA000B57204D0BAF1010F18BFFFDF2FD0BC4875 +:106DB000C0F80080BC49BB480860B06B40F40020BC +:106DC000B063D4F800321021C4F808130020C4F8CE +:106DD0000002DFF8D8C28A03CCF80020C4F8000112 +:106DE000C4F80C01C4F81001C4F80401C4F814017B +:106DF000C4F81801AE4800680090C4F80032C9F821 +:106E00000020C4F80413BAF1010F09D01BE0384682 +:106E10000EF05BF8A748CBF800000220B072C6E77E +:106E20009648006860B917F00C0F09D0C4F80453F5 +:106E3000012000F0E5FDE563256486F83C5002E0A2 +:106E4000002000F0DDFD4FF40020C9F800008D485F +:106E5000C5648D480068404528BFFFDF394640467D +:106E6000BDE8F84F5DE62DE9F0418B4C0646002564 +:106E700094F8310017468846002808BFFFDF16B196 +:106E8000012E16D021E094F83100012808D094F8A2 +:106E90003020394640460DF045FFE16A451814E0C0 +:106EA00094F830103A4640460DF07AFFE16A4518F2 +:106EB0000BE094F8310094F8301001283A4640462F +:106EC00009D00DF095FFE16A45183A46294630464B +:106ED000BDE8F0414AE70DF045FFE16A4518F4E7E7 +:106EE0002DE9F84F694CD4F8000220F00B09D4F8D2 +:106EF00004034FF0100AC0F30018C4F808A30026DA +:106F0000C4F8006269486C490160634D0127A97AA1 +:106F1000012902D0022903D015E0297E11B912E01F +:106F2000697E81B1A97FEA7F07FA01F107FA02F2CF +:106F30001143016095F82000800000F1804000F5C9 +:106F4000C040C0F81065FF208DF80000C4F8106143 +:106F5000276104E09DF80000401E8DF800009DF8B8 +:106F6000000018B1D4F810010028F3D09DF80000FB +:106F7000002808BFFFDFC4F81061002000F040FDCA +:106F80006E72AE72EF72C4F80092B8F1000F18BFC3 +:106F9000C4F804A3BDE8F88FFF2008B58DF8000001 +:106FA0003A480021C0F810110121016105E000BF3D +:106FB0009DF80010491E8DF800109DF8001019B1C1 +:106FC000D0F810110029F3D09DF80000002808BF68 +:106FD000FFDF08BD0068394920F07F400860704736 +:106FE0004FF0E0200221C0F8801100F5C070BFF31F +:106FF0004F8FBFF36F8FC0F8001170474FF0E02143 +:107000000220C1F8000170472D49087070472D49D2 +:107010000860704770B50546EBF738FE1E4C2844F3 +:10702000E16A884298BFFFDF01202074EBF72EFE53 +:10703000144A284400216061C2F8441122490860C2 +:10704000A06B144940F48000A063D001086070BDBB +:1070500070B5114C05461D4A0220207410680E467A +:1070600000F00F00032808BF01223ED0106800F096 +:107070000F00042808BF022237D029E088170040FB +:1070800068150040008000404C8500400010004022 +:107090000000040404F50140DC0B0020ACF50140C5 +:1070A0004885004048810040A8F5014008F50140AE +:1070B000181100400410004000000E043C15004070 +:1070C000C700002004150040448500401015004012 +:1070D000106800F00F0005281BD0106800F00F00AA +:1070E00006281CBFFFDF012213D094F8310094F86A +:1070F0003010012815D028460DF0C1FEFF4960610F +:107100000020C1F844016169E06A0844FC49086054 +:1071100070BDFC48006810F0060F0CBF0822042266 +:10712000E3E7334628460DF078FEE7E7F6494FF4EB +:1071300080000860F548816B21F4800181630021A3 +:1071400001747047C20002F1804202F5F832F04B40 +:10715000C2F81035C2F8141501218140ED480160D4 +:10716000EA48826B114381637047E4480121416022 +:10717000C1600021C0F84411E1480160E348C162E8 +:107180007047E5490860E548D0F8001241F0400139 +:10719000C0F800127047E148D0F8001221F0400119 +:1071A000C0F80012DC49002008607047DB48D0F8C6 +:1071B000001221F01001C0F8001201218161704716 +:1071C000D249FF2081F83E00D4480021C0F81C11AC +:1071D000D0F8001241F01001C0F800127047CF49FA +:1071E00081B0D1F81C21012A0DD0C84991F83E1078 +:1071F000FF290DBF00204942017001B008BF704750 +:10720000012001B07047C64A126802F07F02524264 +:1072100002700020C1F81C01C24800680090EFE72E +:10722000F0B517460C00064608BFFFDFB74D14F057 +:10723000010F2F731CBF012CFFDF002E0CBF01209C +:1072400002206872EC7201281CBF0228FFDFF0BD2B +:10725000AE4981F83F0070472DE9F84FDFF8C8A22A +:107260009AF80000042828BFFFDFA84CDFF89882B6 +:10727000AA4D94F83C0000260127E0B1D5F804019E +:1072800010F1000918BF4FF00109D5F810010028CE +:1072900018BF012050EA09014FF4002B17D08021BC +:1072A000C5F80813C8F800B084F83C6090F0010FEE +:1072B00018BFBDE8F88FDFF84492D9F84C010028D8 +:1072C0007ED0A07A01287CD002287BD0AEE0D5F811 +:1072D0000001DFF84CA230B3C5F800616F61FF20F8 +:1072E000009002E0401E009005D0D5F81C01002857 +:1072F0000098F7D000B9FFDFDAF8000000F07F0A4D +:1073000094F83F0050453CBF002000F079FB84F822 +:107310003EA0C5F81C61C5F808738248006800905B +:107320002F64AF6302E0B9F1000F03D0B9F1000F91 +:107330002BD05DE0DAF8000000F07F0084F83E001A +:10734000C5F81C6194F83D1049B194F83F10814292 +:1073500018D2002000F054FB2F64AF6312E0734991 +:10736000096894F83F308AB2090C984203D30F2A77 +:1073700006D9022904D2012000F042FB2F6401E06B +:107380002F64AF636748006800908022C5F804232B +:107390005A48876466490B68A1F1040CDCF800C008 +:1073A00043F698273B44634519D20A6842F21073AA +:1073B0001A440A60C0F848615F495E48086002E00C +:1073C00034E01CE01EE0091F5C4808605148C0F82A +:1073D00000B0A06B40F40020A063BDE8F88F0E6001 +:1073E000C0F84861C5F80823C8F800B0C0F8486183 +:1073F0008020C5F80803C8F800B0BDE8F88F207EEB +:1074000010B913E0607E88B1A07FE17F07FA00F039 +:1074100007FA01F10843C8F8000094F82000800042 +:1074200000F1804000F5C040C0F810653648A16BFF +:107430000160A663217C002019B1D9F8441101290B +:1074400000D00021A27A012A56D0022A55D000BFCE +:10745000D5F8101101290CBF1021002141EA0008C4 +:107460003748016811F0FF0F03D0D5F81411012936 +:1074700000D0002184F83210006810F0FF0F03D014 +:10748000D5F81801012800D0002084F833002D48D9 +:10749000006884F83400FFF77CF8012818BF00204A +:1074A00084F83500C5F80061C5F80C61C5F81061B5 +:1074B000C5F80461C5F81461C5F818612248006870 +:1074C00000900E48C0F8446120480068DFF8309012 +:1074D0000090D9F80000A062A9F104000068E06201 +:1074E0001B48016801F00F01032908BF012042D0A9 +:1074F000016801F00F012DE045E04BE00080004005 +:10750000448500401414004008F50140DC0B0020C5 +:107510000411004004F501406015004000100040D7 +:10752000481500401C110040C700002074150040A1 +:107530004885004014100040ACF5014048810040EF +:1075400040160040101400401811004044810040D3 +:1075500010150040042908BF02200CD0016801F07A +:107560000F01052925D0006800F00F0006281CBF78 +:10757000FFDF01201DD084F83000A07A84F83100AC +:1075800002282BD11DE0D5F80C01012814BF0020E2 +:1075900008205DE7D5F80C01012814BF0020022067 +:1075A000F64A1268012A14BF04220022104308433D +:1075B0004EE7F348006810F0060F0CBF08200420C7 +:1075C000D9E7607850B1EF49096809780840217817 +:1075D00031EA000008BF84F8247001D084F82460E8 +:1075E00018F0020F0AD0EBF751FBA16AE64A081A1D +:1075F0009AF80010490852F82110884718F0010F36 +:1076000018BF4FF0000B11D0EBF740FBE16A9AF87E +:107610000020081ADD4951F822205946904700BF42 +:107620009AF8000010F0010F2FD10CE018F0020FB3 +:1076300018BF4FF0010BE7D118F0080F18BF4FF03B +:10764000020BE1D1ECE7DFF83CB3DBF80000007897 +:1076500000F00F00072828BF84F8256015D2DBF85A +:107660000000062200F10901A01C0DF05BFF40B9EB +:10767000207ADBF800100978B0EBD11F08BF012099 +:1076800001D04FF0000084F82500E17A4FF00000AF +:1076900011F0020F1CBF18F0020F18F0040F19D1DF +:1076A00011F0100F1CBF94F83320002A02D094F878 +:1076B00035207AB111F0080F1CBF94F82420002A5D +:1076C00008D111F0040F02D094F8251011B118F070 +:1076D000010F01D04FF00100617A19B168B1FFF7D5 +:1076E000FFFB10E0AB48AA490160D5F8000220F08A +:1076F0000300C5F80002E77205E001290DD0022958 +:1077000018BFFFDF10D018F0010F17D0A2489AF869 +:10771000001050F82100804756E06672E772A772A9 +:107720009621227B002006E06672E7720220A0729A +:10773000227B96210120FFF796FBE4E718F0020F69 +:107740002DD018F0040F21D10CF07FFFF0B90CF010 +:107750008EFFD8B991480168001F0068C0F3006C23 +:10776000C0F3425500F00F03C0F30312C0F303202F +:10777000BCF1000F0AD0002B1CBF002A002805D145 +:10778000002918BF032D38BF48F0040827EA9800E5 +:1077900083499AF8002051F82210884714E018F025 +:1077A000080F06D07F489AF8001050F82100804753 +:1077B0000AE018F0100F08BFFFDF05D07A489AF8EA +:1077C000001050F821008047A07A022818BFBDE8B9 +:1077D000F88F207C002808BFBDE8F88F7349C1F8F6 +:1077E0004461022814D0012818BFFFDFE16A6069F4 +:1077F000884298BFFFDF6069C9F80000A06B4FF4B2 +:10780000800140F48000A06369480160BDE8F88F02 +:107810006169E06A0844EFE738B5664D0024002846 +:1078200018BFC5F800426448006864498A7A012A92 +:1078300002D0022A03D018E00A7E12B915E04A7E6F +:107840009AB18B7F012291F81FC002FA03F302FA6A +:107850000CF21A434F4B1A6091F82010890001F185 +:10786000804101F5C041C1F810450121FFF759F9E8 +:10787000C5F80041C5F80C41C5F81041C5F80441F0 +:10788000C5F81441C5F818414D480068009038BD4E +:10789000012804BF28207047022804BF1820704721 +:1078A000042812BF08284FF4A870704700B5FFDF06 +:1078B000282000BD012804BF41F6A47070470228AB +:1078C00004BF41F288307047042804BF46F2180014 +:1078D0007047082804BF47F2A030704700B5FFDFAB +:1078E00041F6A47000BD10B502280DD0012804BFD8 +:1078F00042F6CE3010BD042817BF082843F6A44036 +:10790000FFDF41F66A0010BD0CF07AFE30B90CF0D2 +:1079100084FE002808BF41F6583001D041F264309F +:1079200041F29A01084410BD012812BF022800202C +:107930007047042812BF08284FF4C870704700B57C +:10794000FFDF002000BD1B490820C1F800021149DB +:107950000F4808601C491B480860091D1B48086047 +:107960001C491B480860091D1B48086010494FF45A +:10797000602008601149022088727047001400409E +:107980001414004004150040005C0200485C020032 +:107990000000040408F50140085C02005414004093 +:1079A000185C0200285C0200385C02000080004085 +:1079B00004F501400010004040850040DC0B002031 +:1079C000181100400011004098F5014014100040CB +:1079D0001C110040A8F50140101000401948016832 +:1079E00003291BBF006802280120002070471548AA +:1079F00001680B291BBF00680A280120002070477E +:107A000011490968C9B9114A1149136870B123F0C5 +:107A1000820343F07D0343F0004313600A6822F0C1 +:107A2000100242F0600242F0004205E023F0004301 +:107A300013600A6822F000420A60064981F83D009E +:107A40007047000050150040881700403C17004068 +:107A50007C170040DC0B002010B53F4822210DF0C0 +:107A60000CFE3D480024017821F010010170012135 +:107A700006F064F839494FF6FF7081F82240888497 +:107A800037490880488010BD704734498A8C82424B +:107A900018BF7047002081F822004FF6FF708884DD +:107AA00070472D49016070472D49088070472B4968 +:107AB0008A8CA2F57F43FF3B03D00021016008467A +:107AC000704791F822202549012A1ABF0160012040 +:107AD00000207047214901F1220091F82220012A5B +:107AE00004BF00207047012202701D48008888846E +:107AF000104670471A49488070471849184B8A8CBD +:107B00005B889A4206D191F82220002A1EBF0160AC +:107B100001207047002070471048114A818C52881C +:107B2000914209D14FF6FF71818410F8221F19B1DB +:107B30000021017001207047002070470748084A63 +:107B4000818C5288914205D190F8220000281CBFF8 +:107B50000020704701207047420C00201C0C0020C0 +:107B6000C80000207047574A012340B1012818BFC0 +:107B700070471370086890608888908170475370D0 +:107B80000868C2F802008888D08070474D4A10B15A +:107B9000012807D00EE0507860B1D2F802000860EA +:107BA000D08804E0107828B19068086090898880B7 +:107BB0000120704700207047424910B1012803D0CE +:107BC00006E0487810B903E0087808B10120704752 +:107BD0000020704730B58DB00C4605460D2104A835 +:107BE0000DF06DFDE0788DF81F0020798DF81E00F6 +:107BF00060798DF81D002868009068680190A86879 +:107C00000290E868039068460CF062FB20789DF8CB +:107C10002F1088420CD160789DF82E10884207D131 +:107C2000A0789DF82D10884202BF01200DB030BD14 +:107C300000200DB030BD30B50C4605468DB04FF07C +:107C4000030104F1030012B1FEF7F8F801E0FEF7BA +:107C500014F960790D2120F0C00040F040006071FF +:107C600004A80DF02CFDE0788DF81F0020798DF828 +:107C70001E0060798DF81D002868009068680190EA +:107C8000A8680290E868039068460CF021FB9DF814 +:107C90002F0020709DF82E0060709DF82D00A070C0 +:107CA0000DB030BD10B5002904464FF0060102D0DA +:107CB000FEF7C4F801E0FEF7E0F8607920F0C000BC +:107CC000607110BDCC000020FE48406870472DE96F +:107CD000F0410F46064601461446012005F0F8FA29 +:107CE000054696F85500FFF7E5FD4AF2B121084434 +:107CF0004FF47A71B0FBF1F0718840F27122514378 +:107D0000C0EB4100001BA0F2653403F03DF80028F1 +:107D100018BF1E3CAF4234BF28463846A04203D2AB +:107D2000AF422CBF3C462C467462BDE8F0812DE981 +:107D3000FF4F95B0044690F8550089461190DDE953 +:107D4000171008431390E048002605780C2D28BF33 +:107D5000FFDFDE4F37F8158094F874510C2D28BFE3 +:107D6000FFDFDA4830F8150040441FFA80F894F835 +:107D700065000D280CBF012000200C9017980028EA +:107D800004BF94F8140103282BD10C9848B3B4F81D +:107D90009601484525D1D4F81C01C4F80801608833 +:107DA00040F2E2414843C4F80C01B4F86201B4F86F +:107DB000EE100844C4F81001204602F0EFFFB4F8BA +:107DC0009A01E08294F898016075B4F89C01608093 +:107DD000B4F89E01A080B4F8A001E080022084F8ED +:107DE0001401D4F86C011090D4F868010F90B4F825 +:107DF000EE70B4F86001D4F85C110891179921B1C4 +:107E000094F8281151B100F0DDB804F1E8010391B4 +:107E100074310D9104F5A475091D07E004F59E71F8 +:107E20000391091D0D9104F59675091D0E91B4F885 +:107E30005810A9EB0000A9EB01010FFA80FA0FFA24 +:107E400081FBBAF1000F05DAD4F85801089001203F +:107E5000DA461390002002909B480079E8B3F3F7CC +:107E600039FFD0B3B4F80001022836D394F81401D6 +:107E7000022832D094F82B0178BB94F87481B8F1C1 +:107E80000C0F28BFFFDF914830F8180000F5C860DC +:107E90001FFA80F894F8140101287DD0618840F21F +:107EA000E24041430020B8F1000F05D0884808FBAC +:107EB00001F1B1FBF0F0401C07EB0B01A1EB0A0252 +:107EC000D4F81C1180B2431A029902FB03110291EB +:107ED000C4F81C01012084F82B0194F81401002837 +:107EE00074D0012800F04682022800F09481032813 +:107EF00018BFFFDF00F078820298311A0898FCF76B +:107F0000BCFB0D99012640F2712208600E98A0F882 +:107F10000090002028702E710D980068A86061887C +:107F2000D4F81C015143C0EB41006749A0F2353041 +:107F30000862C969814287BF03990860039801609C +:107F40000398616A0068084400F2A510E86002F036 +:107F50001BFF10B1E8681E30E8606E71B4F8D800FD +:107F6000A0EB090000B20028C4BF032068710C9880 +:107F70000028189800F09A82D8B100BFB4F8001118 +:107F800000290CBF0020B4F80201A4F8020194F803 +:107F90000421401C504300E019E0884209D268796E +:107FA000401E002805DD6E71B4F80201401CA4F8E3 +:107FB00002011798002800F0A18294F828010028F7 +:107FC00000F0988219B00220BDE8F08F65E094F8C7 +:107FD0006800032857D03B4894F8551090F83000BB +:107FE00005F023FBE18A40F27122514300EB41018D +:107FF0000020D4F80C21B8F1000F06D0344808FB5B +:1080000002F2B2FBF0F000F10100D4F80831D4F82C +:108010001021A0EB030C029BC4F8080102FB0C33F7 +:108020004FF0000007D000BF294808FB01F1B1FB69 +:10803000F0F000F10100D4F81811C4F81801A0EB19 +:1080400001011944608840F2E24300FB03F34FF062 +:10805000000006D01E4808FB03F3B3FBF0F000F16C +:10806000010007EB0B03A3EB0A03A3EB0202D4F816 +:108070001C31A2F10102A0EB030302FB03110291E8 +:10808000C4F81C0126E7E18A40F27122D4F80C0101 +:1080900001FB02F100EB4101AAE70F98002808BF9D +:1080A000FFDF94F85510074890F8300005F0BDFA4E +:1080B0000790E18A40F271204143079800EB4101AB +:1080C000002007E0640C0020DC000020585C020067 +:1080D00040420F00B8F1000F07D000BFFF4808FB77 +:1080E00001F1B1FBF0F000F10100C4F81801618862 +:1080F00040F2E24001FB00F14FF0000006D0F748EB +:1081000008FB01F1B1FBF0F000F10100C4F81C0123 +:1081100086B221464FF00100D4F828A005F0D8F827 +:10812000074694F85500FFF7C5FB4AF2B12B5844B7 +:108130004FF47A78B0FBF8F0618840F27122514335 +:10814000C0EB4100801BA0F2653602F01DFE002846 +:1081500018BF1E3EBA4534BF38465046B04203D21F +:10816000BA452CBF56463E46666294F85500FFF766 +:10817000DBFB00F2E140B0FBF8F10F980E1894F829 +:108180005500FFF7D1FB074694F85500FFF792FB27 +:1081900038444AF2AB310844B0FBF8F1E28A40F2CD +:1081A000712042430798D4F8187100EB4200401A3E +:1081B000C01B3044A0F12006617D40F2E24011FB7B +:1081C00000FA94F85500009010F00C0F0ABF0098C8 +:1081D0004EF62830FFF76EFB5844B0FBF8F000EB8A +:1081E000470000EB0A070098FFF752FB384400F104 +:1081F0006201BB48C16194F85500FFF795FB00F29E +:10820000E140B0FBF8F10F980844301AB0F53D7F1B +:1082100098BFFFDF70E6E18A40F27122D4F80C01CA +:10822000514300EB41010020B8F1000F07D000BF1F +:10823000AA4808FB01F1B1FBF0F000F10100C4F81D +:108240001801608840F2E24100FB01F14FF00000AC +:1082500006D0A24808FB01F1B1FBF0F000F10100EB +:10826000C4F81C0186B221464FF00100D4F828A0C2 +:1082700005F02EF8804694F85500FFF71BFB4AF2F4 +:10828000B12B00EB0B014FF47A70B1FBF0F0618879 +:1082900040F271225143C0EB4100801BA0F26536D1 +:1082A00002F072FD002818BF1E3EC24534BF404692 +:1082B0005046B04203D2C2452CBF5646464666627F +:1082C0000FBB1898F8B194F855603046FFF7F2FAF2 +:1082D00000EB0B014FF47A70B1FBF0F0D4F81811F9 +:1082E000E38A084440F27122D4F80C115A4301EB9E +:1082F00042010F1A3046FFF7CBFA1099081A38449A +:10830000A0F120060AE0E18A40F27122D4F80C01C3 +:10831000514300EB4100D4F81811461AD4F810214B +:10832000D4F80811D4F8180101FB020A607D40F26C +:10833000E24110FB01F894F8557017F00C0F0ABFDA +:1083400038464EF62830FFF7B5FA00EB0B014FF434 +:108350007A70B1FBF0F000EB4A0080443846FFF73A +:1083600097FA404400F160015D48C161012084F842 +:108370001401C1E5618840F271225143D4F81C0117 +:10838000D4F81021C0EB410101FB0AF607EB0B0109 +:10839000891AD4F808C1D4F81831491E0CFB0232EE +:1083A00001FB002A607D40F2E24110FB01F894F8E5 +:1083B000557017F00C0F0ABF38464EF62830FFF7FD +:1083C00079FA4AF2B12101444FF47A70B1FBF0F02E +:1083D00000EB4A0080443846FFF75AFA404400F167 +:1083E00060013F48C16187E5628840F27121D4F89D +:1083F0001C015143C0EB410000FB0AF694F86400F5 +:1084000024281CBF94F8650024280BD1B4F89601E9 +:10841000A9EB000000B2002804DB94F899010028C1 +:1084200018BF1190139800B3FFB9109800281ABF15 +:108430000F980028FFDF94F8550010F00C0F14BFC0 +:108440004EF62830FFF736FA4AF2B12101444FF4D4 +:108450007A70B1FBF0F0361A94F85500FFF718FA6D +:108460001099081A3044A0F12006D4F81C1107EB2B +:108470000B0000FB01F7119810F00C0F0ABF1198C8 +:108480004EF62830FFF716FA4AF2B12101444FF4B4 +:108490007A70B1FBF0F000EB47071198FFF7F8F99D +:1084A000384400F160010E48C16125E500287FF4E1 +:1084B00065AD94F8140100283FF47BAD618840F26B +:1084C0007122D4F81C015143C0EB4101284604F04D +:1084D000CFFD0004000C3FF46CAD03E040420F0000 +:1084E000DC0000202299002918BF0880012019B063 +:1084F000BDE8F08F94F86401FCF723FF94F8640161 +:108500002946FCF703FE20B1179880F0010084F89B +:10851000290119B00020BDE8F08F70B5FE4C607ADB +:1085200000281CBF002070BD94F8340038B1A16B46 +:10853000606A884203D9F7F7BEF8002070BDA06AD0 +:10854000E8B1F6F750F90546F5F7C4FF284442F2C2 +:1085500010714618FCF790FB05462946E06AFDF7C6 +:10856000A4F8E562A16A8219914224BF081AA062A8 +:1085700005D20120A062F7F79EF8002070BD01200F +:1085800070BDF8B5E44C02460025E44E6168606AAF +:10859000052A4ED2DFE802F003353A3D4400A07AC6 +:1085A000002760B101216846FDF748FC9DF80000F6 +:1085B00042F210710002B0FBF1F201FB1207F6F774 +:1085C00012F9C119A069FCF758F8A06125740320BD +:1085D00060757079002814BF012003202075607A2F +:1085E00038B9207B04F11001FCF790FD002808BF8A +:1085F000FFDF2584FCF74AFAB079BDE8F840EAF7D6 +:108600008BBCBDE8F840002100F0C7BDC1F868018F +:10861000F8BDD1F86801BDE8F840012100F0BDBD0A +:1086200084F83450FCF732FAB079BDE8F840EAF744 +:1086300073BCFFDFF8BD2DE9F04FDFF8DC820446A4 +:1086400083B098F800008B4601270025B34E4FF009 +:108650000209032804BF98F80C00A04240F0E7800C +:10866000D8F80400B06198F80000032818BFFFDFB5 +:108670000324BBF1080F80F0D680DFE80BF0040F75 +:1086800031312CD4D4CBC8F82450F6F783FC002821 +:1086900018BFFFDFB47003B0BDE8F08FF5F71AFF25 +:1086A0000446D8F81C00A04228BFC8F81C4005D2D8 +:1086B000201AFDF72EF8C8F81C4038B1F6F7E3FF92 +:1086C000002818BFFFDF03B0BDE8F08F03B0002023 +:1086D000BDE8F04F55E703B0BDE8F04FFEF7BCBD75 +:1086E00070794FF0010A002814BF0120032088F898 +:1086F000140088F8105098F8340042F2107B68B1EA +:108700004FF47A71D8F81800FBF7B7FFC8F81800D3 +:10871000002108F1100004F0ABFC1CE001216846C8 +:10872000FDF782FB9DF800000002B0FBFBF10BFBA4 +:10873000110AF6F758F800EB0A018A46D8F8180033 +:10874000FBF79BFFC8F81800514608F1100004F031 +:108750008FFC00F1010AB8F82000411C0A293CBF37 +:108760005044A8F82000D8F8040038B1B8F8200028 +:10877000401C0A2828BF88F8159001D288F81540B7 +:1087800098F8090070BB98F8340040B1D8F8381058 +:10879000D8F82400884202D9F6F78DFF22E0D8F8F5 +:1087A000280058B3F6F71FF80446F5F793FE204467 +:1087B00000EB0B09FCF760FA04462146D8F82C00C0 +:1087C000FCF773FFC8F82C40D8F8281000EB09021A +:1087D000914224BF081AC8F828000FD2C8F82870A0 +:1087E000F6F769FF98F80C00FCF727FA88F80050B4 +:1087F000B07903B0BDE8F04FEAF78EBB98F80C00F3 +:1088000008F11001FCF782FC002808BFFFDF03B06D +:10881000BDE8F08F98F80C00FCF70FFA88F80050CC +:1088200003B0BDE8F08FFFDF03B0BDE8F08F202C70 +:1088300028BFFFDFDFF8E880072138F81400FAF7D7 +:10884000D9F85FEA000A08BFFFDF202C28BFFFDF4E +:1088500038F81400BAF80010884218BFFFDF5446F9 +:10886000C6F818A04FF0200ABBF1080F80F04A812B +:10887000DFE80BF0049FA9A9A2F4F3F2C4F8685151 +:108880003581C4F86C5194F8290138B9FCF7F4F932 +:10889000D4F83411FCF709FF00281BDCB4F82611CA +:1088A000B4F85800814206D1B4F8DC10081AA4F8D4 +:1088B000DE00204605E0081AA4F8DE00B4F8261110 +:1088C0002046A4F85810D4F85011C4F83411C0F858 +:1088D00058111DE0B4F82411B4F85800081AA4F88F +:1088E000DE00B4F824112046A4F85810D4F834114E +:1088F000C4F85011C4F85811D4F83C11C4F8E81069 +:10890000D4F84011C4F85C11B4F84411A4F8601113 +:1089100002F020F906E00000640C0020DC000020DA +:10892000A00C0020FCF782F9804694F85500FEF771 +:10893000C1FF4AF2B12108444FF47A71B0FBF1F063 +:10894000D4F81C1140F27122084461885143C0EBF5 +:108950004100A0F1300AB8F1B70F98BF4FF0B70847 +:108960002146012004F0B4FC4044AAEB0000A0F230 +:108970001A38A2462146012004F0AAFC00F19C010D +:10898000DAF82400884288BF451AC6F810804545A9 +:1089900028BF4546F560D4F85401A0F2A5107061D7 +:1089A000FCF750FE84F8287186F8029003B0BDE809 +:1089B000F08F02F0E4F901E0FEF74EFC84F8287134 +:1089C00003B0BDE8F08FFCF757F9D4F85821014601 +:1089D0001046FCF76AFE48B1628840F27123D4F871 +:1089E0001C115A43C1EB4201B0FBF1F094F8651041 +:1089F0000D290FD0B4F85820B4F826111318994255 +:108A0000AEBF481C401C1044A4F8260194F82A016B +:108A100078B905E0B4F82601401CA4F8260108E066 +:108A2000B4F82601B4F8DC10884204BF401CA4F856 +:108A30002601B4F862010DF1040B401CA4F8620198 +:108A4000B4F88000B4F87E10401AB4F85810401EF4 +:108A500008441FFA80F912E046E03EE052E00023AD +:108A60001A462046CDF800B0FFF761F9002804BF90 +:108A700003B0BDE8F08F012818BFFFDF25D0B4F8A0 +:108A80002611A9EB010000B20028E8DA082084F8DA +:108A9000740084F87370204601F01EFE84F81451AF +:108AA00094F864514FF6FF77202D00D3FFDF28F8AC +:108AB000157094F86401FCF7C0F884F864A1B079EB +:108AC00003B0BDE8F04FEAF727BAB4F82601BDF8C5 +:108AD00004100844A4F82601D1E7FEF75DFA03B0BC +:108AE000BDE8F04FFEF7B8BB94F81401042818BF96 +:108AF000FFDF84F8145194F864514FF6FF77202D6E +:108B0000D5D3D3E7FFDF03B0BDE8F08F10B5FA4C43 +:108B1000207850B101206072F6F7E5FD2078032837 +:108B200005D0207A002808BF10BD0C2010BD207B86 +:108B3000FCF707FC207BFCF752FE207BFCF77DF85E +:108B4000002808BFFFDF0020207010BD2DE9F04F86 +:108B5000E94F83B0387801244FF0000840B17C72AF +:108B60000120F6F7C0FD3878032818BF387A0DD0F9 +:108B7000DFF8889389F8034069460720F9F7C3FEB8 +:108B8000002818BFFFDF4FF6FF7440E0387BFCF78A +:108B9000D8FB387BFCF723FE387BFCF74EF8002827 +:108BA00008BFFFDF87F80080E2E7029800281CBFBB +:108BB00090F8141100292AD00088A0421CBFDFF8C9 +:108BC00040A34FF0200B3AD00721F9F713FF040020 +:108BD00008BFFFDF94F86401FCF701FE84F81481FC +:108BE00094F864514FF6FF76202D28BFFFDF2AF856 +:108BF000156094F86401FCF720F884F864B16946C4 +:108C00000720F9F780FE002818BFFFDF12E0684652 +:108C1000F9F757FE0028C8D011E0029800281CBFC1 +:108C200090F81411002905D00088A0F57F41FF3984 +:108C3000CAD104E06846F9F744FE0028EDD089F86F +:108C4000038087F8348087F80B8003B00020BDE8EC +:108C5000F08F70B50446AB4890F80004AA4D400967 +:108C600095F800144909884218BFFFDF95F8140DE4 +:108C70004009A64991F800144909884218BFFFDF4E +:108C80009E49002001228C7188700A7048700A7118 +:108C9000C870487198490870BDE8704056E7974918 +:108CA000087070472DE9F843934C064688462078B3 +:108CB000002867D19648FBF764FF2073202861D015 +:108CC000032766602770002565722572AEB1012109 +:108CD00006F58270FDF7D1F80620F9F733FE8146DC +:108CE0000720F9F72FFE96F804114844B1FBF0F283 +:108CF00000FB1210401C86F80401FBF797FF40F2BE +:108D0000F651884238BF40F2F65000F59F7086B2A7 +:108D1000F5F7E0FBE061F5F766FD4FF0010900288B +:108D200033D084F80A90FBF7A7FF814601216846FB +:108D3000FDF77AF89DF8000042F210710002B0FBD6 +:108D4000F1F201FB120081194846FBF796FCA06185 +:108D5000C4E90A8969484079002814BF012003202A +:108D6000207567752574207B04F11001FCF7CEF99E +:108D7000002808BFFFDF25840020F6F7B4FC0020A0 +:108D8000BDE8F8830C20BDE8F883FBF775FF31469A +:108D9000FBF773FCA061A57284F83490A8F28B50A5 +:108DA000A562A063D6E7554948717047534948709A +:108DB00070475249087170472DE9F0414F4C064603 +:108DC0002089401C2081D4E903516078D6F868716D +:108DD00020B13A46284604F076F90546E068854217 +:108DE00005D06169281A08446061FCF72BFCE56036 +:108DF000AF4209D896F81401012805D0E078002880 +:108E000004BF0120BDE8F0810020BDE8F08110B56D +:108E100004460846FEF74EFD4AF2B12108444FF4DD +:108E20007A71B0FBF1F040F2E241614300F235307B +:108E300081428CBF081A002010BD70B5044682B074 +:108E4000002084F8280194F8E600002807BF94F871 +:108E50001401032802B070BDFBF70EFFD4F85821AF +:108E600001461046FCF721FC0028DCBF02B070BDB3 +:108E7000628840F27123D4F81C115A43C1EB4201BD +:108E8000B0FBF1F0B4F85810401C0844A4F82401D9 +:108E9000B4F8DC00B4F82421801A00B20028DCBF4A +:108EA00002B070BD012084F82A01B4F88000B4F843 +:108EB0007E2001AE801A401E084485B212E0009662 +:108EC000B4F82411002301222046FEF730FF0028C9 +:108ED00004BF02B070BD01281CD0022812BFFFDF02 +:108EE00002B070BDB4F82401281A00B20028BCBF3B +:108EF00002B070BDE3E70000640C0020DC0000203D +:108F0000A00C002001E000E00BE000E019E000E030 +:108F100037860100B4F82401BDF804100844A4F811 +:108F20002401DFE7F8B50422002506295BD2DFE83B +:108F300001F007260319192A044680F8142107E0D6 +:108F40000446BD48C078002818BF84F814210AD010 +:108F5000FBF79CFDA4F86251B4F85800A4F8260170 +:108F600084F82A51F8BD0095B4F8DC1001230022E2 +:108F70002046FEF7DCFE002818BFFFDFE8E70321EC +:108F800080F81411F8BD0646876AB0F81C01314616 +:108F900085B2012004F09CF9044696F85500FEF7CE +:108FA00089FC4AF2B12108444FF47A71B0FBF1F028 +:108FB000718840F271225143C0EB4100401BA0F286 +:108FC000653501F0E1FE002818BF1E3DA74234BF01 +:108FD00020463846A84228BF2C4602D2A74228BFC6 +:108FE0003C467462F8BDFFDFF8BD2DE9F05F924E9C +:108FF000B178022906BF31890029BDE8F09FB46924 +:10900000C4F86C0194F85500FEF742FCD4F86C11DA +:10901000081AF1680144F160316908443061B469AB +:1090200094F82B01002808BFBDE8F09F94F81401C4 +:10903000032818BFBDE8F09F94F8555036780C2EE1 +:1090400028BFFFDF7D4F37F8168094F874610C2E2F +:1090500028BFFFDF37F81600404494F8748186B2C9 +:10906000B8F10C0F28BFFFDF37F8180000F5C86013 +:109070001FFA80F82846FEF70BFCD4F86C114FF06D +:10908000000A0F1A15F00C0F0ABF28464EF62830BA +:10909000FEF710FC4FF47A7900F2E730B0FBF9F0FC +:1090A0003F1A2846FEF7F4FBD4F8E81015F00C0F31 +:1090B000A1EB000B0ABF28464EF62830FEF7FAFB5C +:1090C0004AF2B1210844B0FBF9F0ABEB0000A0F18B +:1090D00060017143B1FBF8F1292202EB50006031CD +:1090E000A0EB510200EB5100BA4201D8B84201D8BE +:1090F000F2F794FE608840F2E241414300202EB135 +:1091000006FB01F04E49B0FBF1F0401CC4F81C0115 +:1091100084F82BA1BDE8F09F70B50546464890F84D +:1091200002C0BCF1020F07BF806900F5B474454866 +:1091300000F12404002904BF256070BD4FF47A7645 +:1091400001290DD002291CBFFFDF70BD1046FEF7BC +:10915000CAFB00F2E140B0FBF6F0281A206070BDB7 +:109160001846FEF7E1FB00F2E140B0FBF6F0281AEA +:10917000206070BD3348007800281CBF0020704775 +:1091800010B50720F9F7D0FB80F0010010BD2D4885 +:109190000078002818BF012070472DE9F843294CBA +:1091A0000025814684F83450D4F8188084F83010B3 +:1091B000E5722570012727722946606803F0CDFA11 +:1091C0006168C1F85881267B81F86461C1F86891B3 +:1091D000C1F85C81B1F80080202E28BFFFDF1A485B +:1091E00020F81680646884F814510023A4F86051B4 +:1091F0001A46194620460095FEF799FD002818BF2B +:10920000FFDFC4F81051C4F8085184F81471A4F8B1 +:109210002651A4F8245184F82A51B4F85800401E6D +:10922000A4F85800A4F86251FBF730FC024880799A +:10923000BDE8F843E9F770BEDC000020585C02008E +:1092400040420F00640C0020A00C0020012804D034 +:10925000022805D0032808D105E0012907D004E041 +:10926000022904D001E0042901D000207047012028 +:1092700070472DE9F0410E46044604F07CFD05469A +:10928000204604F07CFD044604F097F8FE4F0100F0 +:1092900015D0386990F854208A4210D090F8AC313B +:1092A0001BB190F8AE3123421FD02EB990F8513047 +:1092B000234201D18A4218D890F8AC01A8B12846BF +:1092C00004F07BF870B1396991F85520824209D0D9 +:1092D00091F8AC0118B191F8AF01284205D091F88E +:1092E000AC0110B10120BDE8F0810020FBE730B5F2 +:1092F000E54C85B0E06900285DD0142168460CF08B +:10930000DEF9206990F85500FEF7D4FA4FF47A712F +:1093100000F5FA70B0FBF1F5206990F85500FEF702 +:10932000B7FA2844ADF8060020690188ADF80010AE +:10933000B0F85810ADF804104188ADF8021090F85C +:109340008E0130B1A069C11C039104F0F5FB8DF8CA +:109350001000206990F88D018DF80800E1696846D9 +:1093600088472069002180F88E1180F88D110399BB +:10937000002920D090F88C1100291CD190F864109D +:10938000272918D09DF81010039A002913D01378BC +:109390000124FF2B11D0072B0DD102290BD15178BD +:1093A000FF2908D180F88C410399C0F890119DF8ED +:1093B000101080F88F1105B030BD1B29F2D9FAE7E3 +:1093C00070B5B14C206990F865001B2800D0FFDF14 +:1093D0002069002580F88D5090F8C00100B1FFDFB2 +:1093E000206990F88E1041B180F88E500188A0F865 +:1093F000C41180F8C2510E2108E00188A0F8C41100 +:1094000080F8C251012180F8C6110D2180F8C011E9 +:109410000088F9F721FCF9F7B9F82079E9F77CFD24 +:10942000206980F8655070BD70B5974CA0798007B1 +:109430002CD5A078002829D162692046D37801690B +:109440000D2B01F158005FD00DDCA3F102034FF0AA +:1094500001050B2B19D2DFE803F01A1844506127DD +:10946000182C183A6400152B6FD008DC112B4BD048 +:10947000122B5AD0132B62D0142B06D166E0162B78 +:1094800071D0172B70D0FF2B6FD0FFDF70BD91F81C +:1094900067200123194603F081FD0028F6D12169D8 +:1094A000082081F8670070BD1079BDE8704001F0B8 +:1094B00008BD91F86600C00700D1FFDF01F0C0FCD5 +:1094C000206910F8661F21F00101017070BD91F84C +:1094D0006500102800D0FFDF2069112180F88D5031 +:1094E00008E091F86500142800D0FFDF20691521FD +:1094F00080F88D5080F8651070BD91F865001528D2 +:1095000000D0FFDF172005E091F86500152800D096 +:10951000FFDF1920216981F8650070BDBDE870404A +:109520004EE7BDE8704001F0A0BC91F86420012333 +:10953000002103F033FD00B9FFDF0E200FE011F82A +:10954000660F20F0040008701DE00FE091F8642021 +:109550000123002103F022FD00B9FFDF1C20216957 +:1095600081F8640070BD12E01BE022E091F8660013 +:10957000C0F30110012800D0FFDF206910F8661F3A +:1095800021F010010170BDE8704001F059BC91F864 +:1095900064200123002103F001FD00B9FFDF1F203B +:1095A000DDE791F86500212801D000B1FFDF22201E +:1095B000B0E7BDE8704001F04FBC3348016991F855 +:1095C0006620130702D501218170704742F008021E +:1095D00081F866208069C07881F8C90001F027BC55 +:1095E00010B5294C21690A88A1F8042281F80202E9 +:1095F00091F8540001F009FC216981F8060291F804 +:10960000550001F002FC216981F80702012081F870 +:109610000002002081F8AC012079BDE81040E9F794 +:109620007BBCF0B4184C206900F5DA730188198509 +:10963000018E5985818E9985018FB0F84420914221 +:1096400000D31146D985828FB0F846108A4200D2E5 +:109650001146198690F855204FF0010512F00C0FB5 +:109660004FF4296203D0914200D81146198690F830 +:10967000540010F00C0F04D0988D904200D902468F +:109680009A8583F8265001E0000100202079F0BC83 +:10969000E9F742BC10B5F84C01230921206990F884 +:1096A0006420583003F07AFC38B12169002001F8B9 +:1096B0007C0F087301F8180C10BD0120A07010BDBC +:1096C00070B5ED4D012329462869896990F8642019 +:1096D00009790E2A01D1122903D000241C2A03D0B3 +:1096E00004E0BDE87040D5E7142902D0202A08D054 +:1096F00009E080F8644080F88840BDE8704001F0DF +:1097000003BC162906D0262A01D1162902D0172912 +:1097100009D00CE000F8644F80F8244040782128FC +:109720000CD01A2017E090F86520222A07D0EA69A9 +:10973000002A03D0FF2901D180F88E3112E780F88A +:10974000654001F07DFB286980F87D4090F8AC0110 +:109750000028F3D00020BDE8704041E72DE9F84330 +:10976000C54C206990F86410202909D05FF00007EB +:1097700090F86510222905D07FB300F1640503E05D +:109780000127F5E700F1650510F8961F41F0040187 +:109790000170A06904F0FBFA4FF00108002608B33D +:1097A0003946A069FFF765FDE0B16A46A169206905 +:1097B00003F012FE90B3A06904F0E7FA2169A1F862 +:1097C0009601B1F8581001F014FB40B3206928212C +:1097D00080F8741080F8738058E0FFE70220A070D2 +:1097E000BDE8F883206990F8AC0110B11E20FFF7A6 +:1097F000F7FEAFB1A0692169C07881F8CA0008FA04 +:1098000000F1C1F3006000B9FFDF20690A2180F890 +:10981000641090F8880040B9FFDF06E009E02AE014 +:109820002E7001F00DFBFFF7C8FE206980F87D6007 +:10983000D6E7226992F8AC0170B1B2F8583092F8CC +:109840005410B2F8B00102F5CB7203F0B7FE68B164 +:109850002169252081F86400206900F1650180F804 +:109860007D608D4212D180F865600FE00020FFF727 +:10987000B7FE2E70F0E720699DF8001080F898116F +:109880009DF8011080F8991124202870206900F1BA +:1098900065018D4203D1BDE8F84301F0D1BA80F8EB +:1098A00088609DE770B5744C01230B21206990F806 +:1098B0006520583003F072FB202650BB206901233D +:1098C000002190F86520583003F068FB0125F0B1C5 +:1098D000206990F8640024281BD0A06904F035FAB0 +:1098E000C8B1206990F8961041F0040180F89610F4 +:1098F000A1694A7902F0070280F85120097901F044 +:10990000070180F8501090F8AD311BBB06E0A57040 +:1099100028E6A67026E6BDE870404EE690F8AC3129 +:10992000C3B900F154035E788E4205D11978914293 +:1099300002D180F87D500DE000F5FD710D700288B8 +:109940004A8090F850200A7190F8510048712079AF +:10995000E9F7E2FA2169212081F86500BDE870404D +:1099600001F065BA70B54448006990F84E20448E05 +:10997000C38E418FB0F84050022A23D0A94200D3B1 +:1099800029460186C18FB0F84220914200D311468A +:109990008186018FB0F84420914200D31146418660 +:1099A000818FB0F84620914200D31146C186418E86 +:1099B000A14200D90C464486C18E994200D90B467B +:1099C000C386CFE5028E914200D31146C68F828EA8 +:1099D000964200D23246A94200D329460186B0F809 +:1099E00042108A4200D30A468286002180F84E1037 +:1099F000CFE770B5204C206990F8660010F0300F6A +:109A000004D0A07840F00100A070ABE5A06904F09C +:109A100081F948B32569A06904F078F92887256998 +:109A2000A06904F06FF968872569A06904F070F9EE +:109A3000A8872569A06904F067F9E887A0794FF045 +:109A40000102800703D56069C07814280FD020690F +:109A500090F864101C290AD090F84E10012910D0FB +:109A600090F8A31169B909E0BDE87040A5E5206947 +:109A700080F84E2005E000000001002090F8A211BF +:109A800031B1206910F8661F41F01001017016E035 +:109A900090F8661041F0200180F8661000F5DA7148 +:109AA00003888B86038FCB86438F0B87838F4B87EF +:109AB000C08F888781F832202079E9F72DFABDE838 +:109AC000704001F0B4B970B5FE4C206990F8661092 +:109AD000890707D590F8642001230821583003F046 +:109AE0005DFAE8B1206990F89000800712D4A0696F +:109AF00004F0ECF8216981F89100A06930F8052F95 +:109B0000A1F892204088A1F8940011F8900F40F03D +:109B100002000870206990F89010C90703D00FE088 +:109B20000120A0701EE590F86600800700D5FFDFD9 +:109B3000206910F8661F41F00201017001F077F909 +:109B40002069002590F86410062906D180F8645039 +:109B500080F888502079E9F7DFF9206990F89411AE +:109B60000429DFD180F894512079E9F7D5F92069EB +:109B700090F864100029D5D180F88850F2E470B5CF +:109B8000D04C01230021206990F86520583003F063 +:109B900005FA012578B9206990F86520122A0AD0C3 +:109BA00001230521583003F0F9F910B10820A07005 +:109BB000D8E4A570D6E4206990F88E0008B901F0C9 +:109BC00036F92169A069F03104F061F82169A069D2 +:109BD000C03104F067F8206990F8C80100B1FFDFD8 +:109BE00021690888A1F8CA0101F5E671A06904F0AD +:109BF0003CF82169A06901F5EA7104F03EF820699A +:109C000080F8C851142180F865102079BDE87040B3 +:109C1000E9F782B970B5AB4C01230021206990F8B7 +:109C20006520583003F0BAF90125A8B1A06903F006 +:109C3000E8FF98B1A0692169B0F80D00A1F896017C +:109C4000B1F8581001F0D5F858B12069282180F8F2 +:109C5000741080F8735085E4A57083E4BDE870400B +:109C6000ABE4A0692169027981F89821B0F8052058 +:109C7000A1F89A2103F0B8FF2169A1F89C01A0691D +:109C800003F0B5FF2169A1F89E01A06903F0B6FFBA +:109C90002169A1F8A0010D2081F8650062E47CB57E +:109CA000884CA079C00738D0A06901230521C57868 +:109CB000206990F86520583003F070F968B1AD1E46 +:109CC0000A2D06D2DFE805F0090905050909050591 +:109CD0000909A07840F00800A070A07800281CD1E5 +:109CE000A06903F057FF00287AD0A0690226C57842 +:109CF0001DB1012D01D0162D18D1206990F86400F6 +:109D000003F034F990B1206990F864101F290DD048 +:109D1000202903D0162D16D0A6707CBD262180F8F0 +:109D20006410162D02D02A20FFF75AFC0C2D58D0B3 +:109D30000CDC0C2D54D2DFE805F033301D44A7A70E +:109D4000479E57A736392020A0707CBD0120152DD5 +:109D500075D008DC112D73D0122D69D0132D64D06D +:109D6000142D3DD178E0162D7CD0182D7DD0FF2DFF +:109D700036D183E020690123194690F867205830D6 +:109D800003F00CF9F8B9A06903F068FF216981F8C4 +:109D90007A01072081F8670078E001F03CF975E06E +:109DA000FFF738FF72E001F016F96FE0206990F8D4 +:109DB0006510112901D0A67068E0122180F86510A5 +:109DC00064E0FFF7DCFE61E0206990F86500172889 +:109DD000F1D101F035F821691B2081F8650055E0CB +:109DE00052E0FFF770FE51E0206990F86600C0076E +:109DF00003D0A07840F001001FE06946A06903F09D +:109E00006CFF9DF8000000F02501206900F8961F06 +:109E10009DF8011001F04901417001F008F8206936 +:109E200010F8661F41F0010114E0FFF733FC2DE04C +:109E3000216991F86610490705D5A07026E00EE06B +:109E400016E00FE011E000F0F2FF206910F8661F45 +:109E500041F00401017019E0FFF7CBFD16E001F0BD +:109E600087F813E0FFF71EFD10E0FFF777FC0DE029 +:109E700001F05DF80AE0FFF723FC07E0E16919B1A2 +:109E8000216981F88E0101E0FFF797FB2069F0E975 +:109E90002A12491C42F10002C0E900127CBD70B5D3 +:109EA000084CA07900074DD5A07800284AD1206938 +:109EB00090F8CC00FE2800D1FFDF2069FE2180F859 +:109EC000CC1001E00001002090F865100025192950 +:109ED00006D180F88D5000F0B3FF206980F86550FE +:109EE000206990F864101F2902D0272921D119E098 +:109EF00090F8650003F03AF878B120692621012333 +:109F000080F8641090F865200B21583003F046F873 +:109F100078B92A20FFF764FB0BE02169202081F843 +:109F2000640006E0012180F88D1180F8645080F80B +:109F30008850206990F86710082903D10221217008 +:109F400080F8CC10E4E4F949096991F898210AB93C +:109F500091F8542081F8542091F899210AB991F888 +:109F6000552081F85520002802D00020FFF738BB8B +:109F7000704770B5ED4C06460D46206990F8CC0050 +:109F8000FE2800D0FFDF2269002082F8CC6015B1E6 +:109F9000A2F88A00BCE422F8840F01201071B7E413 +:109FA00070B5E24C01230021206990F864205830FC +:109FB00002F0F4FF00287AD0206990F8A21111B1C4 +:109FC00090F8A31139B190F8AC1100296ED090F837 +:109FD000AD1111B36AE090F8651024291BD090F8F8 +:109FE0006410242917D0002300F5CC7200F5D1713C +:109FF00003F084F82169002081F8A20101461420B1 +:10A00000FFF7B7FF206930F8421FA0F88C10818855 +:10A01000A0F88E1050E00123E6E790F865200123B8 +:10A020000B21583002F0BAFF68BB206990F8540049 +:10A0300000F0EBFE0646206990F8550000F0E5FEC2 +:10A040000546206990F8AE113046FFF7FFF8D8B109 +:10A05000206990F8AF112846FFF7F8F8A0B12269FF +:10A06000B2F8583092F85410B2F8B00102F5CB7241 +:10A0700003F0A4FA20B12169252081F864001BE0D7 +:10A080000020FFF7ADFA11E020690123032190F8C9 +:10A090006520583002F082FF40B920690123022177 +:10A0A00090F86520583002F079FF08B100202FE4C5 +:10A0B00000211620FFF75DFF012029E410B5E8BB61 +:10A0C0009A4C206990F86610CA0702D00121092035 +:10A0D00052E08A070AD501210C20FFF74AFF2069C8 +:10A0E00010F8901F41F00101017047E04A0702D5C6 +:10A0F0000121132040E00A0705D510F8C91F41715E +:10A100000121072038E011F0300F3BD090F8A31167 +:10A11000A1B990F8A211E1B190F8651024292FD0CF +:10A1200090F8641024292BD05FF0000300F5CC7266 +:10A1300000F5D17102F0E2FF206900E022E010F8A2 +:10A14000661F21F0200141F010010170002180F80C +:10A150003C11206990F86600C00613D5FFF702FC99 +:10A1600000F0D2FE206930F8421FA0F88C108188E0 +:10A17000A0F88E1001211520FFF7FBFE012010BD75 +:10A180000123D3E7002010BD70B5684C206990F81A +:10A19000CC10FE2978D1A178002975D190F86720DC +:10A1A00001231946583002F0F9FE00286CD12069CD +:10A1B00090F8781149B10021A0F8821090F8791137 +:10A1C00080F8CE10002102205BE090F8652001238A +:10A1D0000421583002F0E2FE0546FFF76FFF002829 +:10A1E00052D1284600F07BFF00284DD12069012381 +:10A1F000002190F86420583002F0D0FE78B1206938 +:10A200000123042190F86520583002F0C7FE30B9D0 +:10A21000206990F87C0010B10021122031E0206903 +:10A2200090F864200A2A0DD0002D2DD101230021A1 +:10A23000583002F0B3FE78B1206990F894110429E7 +:10A240000AD105E010F8CA1F01710021072018E0AB +:10A2500090F89000800718D0FFF7A2FE002813D1D5 +:10A2600020690123002190F86420583002F096FE06 +:10A27000002809D0206990F88C01002804D0002122 +:10A28000FF20BDE8704074E609E000210C20FFF7D4 +:10A2900070FE206910F8901F41F00101017041E447 +:10A2A0003EB505466846FDF702FC00B9FFDF2221F6 +:10A2B00000980BF0E2F90321009803F053FC00989A +:10A2C000017821F010010170294603F070FC174C51 +:10A2D0000D2D43D00BDCA5F102050B2D19D2DFE8C3 +:10A2E00005F01F184A19191F185518192700152DA0 +:10A2F0005DD008DC112D28D0122D0BD0132D09D0E4 +:10A30000142D06D153E0162D2CD0172D68D0FF2D1B +:10A3100072D0FFDFFDF7DEFB002800D1FFDF3EBD7E +:10A320002169009891F8CE101AE000000001002089 +:10A33000E26800981178017191884171090A817170 +:10A340005188C171090A0172E4E70321009803F002 +:10A3500038FD0621009803F038FDDBE70098062160 +:10A360000171D7E70098216991F8AE21027191F847 +:10A37000AF114171CEE721690098F83103F096FCE6 +:10A3800021690098C43103F09BFCC3E7F849D1E987 +:10A390000001CDE90101206901A990F8960000F0C3 +:10A3A00025008DF80400009803F0C5FCB2E7206991 +:10A3B000B0F84410009803F095FC2069B0F8D01074 +:10A3C000009803F093FC2069B0F84010009803F067 +:10A3D00091FC2069B0F8CE10009803F08FFC99E74B +:10A3E000216991F8AC0100280098BDD111F8542FD3 +:10A3F00002714978BDE7FFE7206990F88F21D0F816 +:10A400009011009803F0E1FB84E7DA4810B5006989 +:10A4100090F86A1041B990F8652001230621583060 +:10A4200002F0BCFD002800D0012010BD70B5D14D58 +:10A43000286990F8681039B1012905D0022906D0A1 +:10A44000032904D0FFDF06E4B0F8DC1037E090F811 +:10A450006710082936D0B0F87E10B0F880200024AC +:10A460008B1C9A4206D3511A891E0C04240C01D06D +:10A47000641EA4B290F87C1039B190F864200123D6 +:10A480000921583002F08AFD40B3FFF7BEFF78B1D2 +:10A4900029690020B1F87820B1F876108B1C9A4217 +:10A4A00003D3501A801E00D0401EA04200D284B2B6 +:10A4B0000CB1641EA4B22869B0F8DC102144A0F8E5 +:10A4C000D8103FE5B0F87E100329BDD330F8581FEF +:10A4D000028D1144491CA0F8801033E50024EAE7FE +:10A4E00070B50C4605464FF4027120460BF0E7F8B4 +:10A4F000258027E5F8F787BB2DE9F0410D46074693 +:10A500000721F8F777FA041E3CD094F8B40100262E +:10A51000A8B16E70092028700BE0268484F8B4611D +:10A52000D4F8B6016860D4F8BA01A860B4F8BE01E6 +:10A53000A88194F8B4010028EFD12E71BAE094F804 +:10A54000C00190B394F8C0010D2813D00E2801D09B +:10A55000FFDFAFE02088F8F77FFB0746F8F72BF81E +:10A5600078B96E700E20287094F8C2012871208886 +:10A57000E88014E02088F8F76FFB0746F8F71BF82F +:10A5800010B10020BDE8F0816E700D20287094F8A5 +:10A59000C20128712088E88094F8C601287284F8E6 +:10A5A000C0613846F8F701F884E0FFE794F8F80155 +:10A5B00030B16E701020287084F8F861AF8079E0B7 +:10A5C00094F8C80190B16E700A2028702088A88085 +:10A5D000D4F8CC11C5F80610D4F8D011C5F80A107B +:10A5E000B4F8D401E88184F8C86163E094F8D60136 +:10A5F00040B16E701A202870B4F8D801A88084F891 +:10A60000D66157E094F8F20170B16E701B2028708B +:10A6100005E000BF84F8F261D4F8F401686094F8B2 +:10A62000F2010028F6D145E094F8DA0190B16E709D +:10A630001520287004F5EE7707E000BF84F8DA6192 +:10A640000A223946281D0AF0DEFF94F8DA010028B4 +:10A65000F4D12FE094F8E60158B16E701D202870F7 +:10A6600084F8E6610A2204F5F471281D0AF0CBFF94 +:10A6700020E094F8FA0138B11E20287084F8FA61BD +:10A68000D4F8FC01686015E094F8000200283FF45B +:10A6900079AF6E701620287008E000BF84F8006261 +:10A6A000D4F802026860B4F80602288194F8000227 +:10A6B0000028F3D1012065E72E480021C161016225 +:10A6C0000846704730B52B4D0C46E860FFF7F4FFA5 +:10A6D00000B1FFDF2C7130BD002180F8641080F8DC +:10A6E000651080F8681090F8E61009B1022100E0CA +:10A6F0000321FEF717BC2DE9F0411E4C05462069E9 +:10A7000009B1002104E0B0F8EE10B0F8DE201144E9 +:10A71000A0F8EE1090F8781139B990F8672001236D +:10A720001946583002F03AFC30B1206930F8821FE7 +:10A73000B0F85C2011440180206990F8883033B172 +:10A74000B0F88410B0F8DE201144A0F8841090F91D +:10A750008C70002F06DDB0F88A10B0F8DE201144AE +:10A76000A0F88A1001213D2635B180F8746017E009 +:10A77000705C0200000100202278022A0AD0012A1F +:10A7800011D0A2782AB380F8731012F0140F0DD0F4 +:10A790001E2113E090F8CE20062A3CD016223AE083 +:10A7A00080F8731044E090F87A2134E0110702D564 +:10A7B00080F874603CE0910603D5232180F8741082 +:10A7C00036E0900700D1FFDF21692A2081F874006C +:10A7D0002AE02BB1B0F88420B0F886309A4210D22B +:10A7E000002F05DDB0F88A20B0F886309A4208D2F2 +:10A7F000B0F88230B0F88020934204D390F87831DA +:10A800000BB1222207E090F868303BB1B0F87E30FF +:10A81000934209D3082280F87420C1E7B0F87E2063 +:10A82000062A01D33E22F6E7206990F8731019B189 +:10A830002069BDE8F0414FE7BDE8F0410021FEF797 +:10A8400071BB2DE9F047FA4C81460D46206900881E +:10A85000F8F714FA060000D1FFDFA0782843A070B3 +:10A86000A0794FF000058006206904D5A0F87E503D +:10A8700080F8EC5003E030F87E1F491C0180FFF7A0 +:10A88000C4FD012740B3E088000506D5206990F893 +:10A890006A1011B1A0F876501EE02069B0F8761069 +:10A8A000491C89B2A0F87610B0F878208A4201D30A +:10A8B000531A00E00023B4F808C00CF1050C6345FE +:10A8C00001D880F87C70914206D3A0F8765080F8C9 +:10A8D000F8712079E8F720FBA0794FF0020810F01A +:10A8E000600F0ED0206990F8681011B1032908D1CB +:10A8F00002E080F8687001E080F868800121FEF7CE +:10A9000011FB206990F86810012904D1E188C9057C +:10A9100001D580F86880B9F1000F71D1E18889050F +:10A9200002D5A0F8005104E0B0F80011491CA0F8CD +:10A93000001100F09BFBFEF7DAFCFFF725FC00F0AE +:10A9400057FF0028206902D0A0F8E05003E030F85B +:10A95000E01F491C018000F04EFF38B1216991F8D9 +:10A96000EC00022807D8401C81F8EC00206990F820 +:10A97000EC00022804D9206920F8E05F45800573C7 +:10A9800020690123002190F86520583002F006FB71 +:10A9900020B9206990F865000C2859D1206901235D +:10A9A000002190F86420583002F0F8FA48B320698A +:10A9B0000123002190F86720583002F0EFFA00B32D +:10A9C000206990F86810022942D190F8EC00C0B9D3 +:10A9D0003046F7F7C0FBA0B1216991F8CC00FE2802 +:10A9E00036D1B1F8DA00012832D981F8E570B1F832 +:10A9F0008000B1F87E20831E9A4203DB012004E030 +:10AA000032E025E0801A401E80B2B1F8E0202389B0 +:10AA10009A4201D3012202E09A1A521C92B2904249 +:10AA200000D91046012801D181F8E55091F8702134 +:10AA300092B1B1F8E220B1F872118A4201D301213A +:10AA400002E0891A491C89B2884205D9084603E008 +:10AA50002169012081F8E5502169B1F8582010449E +:10AA6000A1F8DC00FFF7E2FCE088C0F34021484693 +:10AA7000FFF741FE206980F8E650BDE8F047FDF79A +:10AA80004BB86B4902468878CB78184312D10846F8 +:10AA9000006942B18979090703D590F86700082851 +:10AAA00008D001207047B0F84810028E914201D8BA +:10AAB000FEF782B90020704770B55D4C05460E4622 +:10AAC000E0882843E080A80703D5E80700D0FFDF2F +:10AAD0006661EA074FF000014FF001001AD0A6614D +:10AAE000F278062A02D00B2A14D10AE0226992F8E1 +:10AAF0006530172B0ED10023E2E9283302F8370C1A +:10AB000008E0226992F86530112B03D182F86910B0 +:10AB100082F88E00AA0718D56269D278052A02D079 +:10AB20000B2A12D10AE0216991F86520152A0CD16F +:10AB30000022E1E92A2201F83E0C06E0206990F8A3 +:10AB40006520102A01D180F86A10280601D5082056 +:10AB5000E07078E42DE9F84F354C00254FF00108FE +:10AB6000E580A570E5704146257061F3070220611C +:10AB70009246814680F8E6800088F8F77FF8070063 +:10AB800000D1FFDF20690088FCF78EFF2069008874 +:10AB9000FCF7B0FF2069B0F8DA1071B190F8CC1072 +:10ABA000FE290FD190F8781191B190F86720012318 +:10ABB0001946583002F0F2F980B1206990F8CC00C3 +:10ABC000FE2805D0206990F8CC0000BFFFF768FB95 +:10ABD000206990F8E71089B1258118E02069A0F874 +:10ABE000825090F8791180F8CE1000210220FFF7F2 +:10ABF000C0F9206980F8E5500220E7E790F8B41129 +:10AC000019B9018C8288914200D881882181B0F8DD +:10AC1000DE10491E8EB2B0F8E0103144A0F8E0100A +:10AC200090F8E41031B1A0F8E25080F8E45006E06A +:10AC300000010020B0F8E2103144A0F8E21030F832 +:10AC40007E1F31440180FFF7E0FB20B1206930F81E +:10AC5000761F314401802069B0F8DA10012902D84A +:10AC6000491CA0F8DA100EB180F8EC5090F8E5100D +:10AC7000A1B1B0F8E000218988420FD23846F7F739 +:10AC80006AFA58B1206990F8701139B1B0F8E21041 +:10AC9000B0F87201814201D300F0B0FD206980F864 +:10ACA000E55090F865100B2901D00C2916D1B0F8A9 +:10ACB0005820B0F89631D21A12B2002A0EDBD0F822 +:10ACC0009811816090F89C110173022101F045FDFB +:10ACD000206980F8655080F8988026E0242910D1FA +:10ACE000B0F85810B0F89621891A09B2002908DB8B +:10ACF00090F8AC01FFF727F9206900F8655F057649 +:10AD000013E090F86410242901D025290DD1B0F862 +:10AD10005810B0F89601081A00B2002805DB01208F +:10AD2000FFF711F9206980F8645020690146B0F8F6 +:10AD3000DE20583001F0E9FE206990F8701109B169 +:10AD4000A0F8E250F9480090F94BFA4A49465046BB +:10AD500000F0AEFC216A11B16078FCF7F3F92069CC +:10AD60000123052190F86520583002F017F90028DA +:10AD700003D0BDE8F84F00F036BABDE8F88F00F018 +:10AD80001DBDED49C8617047EB48C069002800D07F +:10AD900001207047E84A50701162704710B50446B0 +:10ADA000B0F89C214388B0F89E11B0F8A0019A42F7 +:10ADB00005D1A388994202D1E38898420FD0238815 +:10ADC000A4F8B831A4F8BA21A4F8BC11A4F8BE01C3 +:10ADD000012084F8B401D8480079E8F79DF80121F2 +:10ADE000204601F0BAFC002004F8650F0320E07053 +:10ADF00010BD401A00B247F6FE71884201DC0028FF +:10AE000001DC012070470020704710B5012808D0F0 +:10AE1000022808D0042808D0082806D0FFDF2046E2 +:10AE200010BD0124FBE70224F9E70324F7E7C24839 +:10AE30000021006920F88A1F8178491C81707047C1 +:10AE4000BD4800B5016911F88C0F401E40B2087072 +:10AE5000002800DAFFDF00BDB7482721006980F82D +:10AE60006410002180F88C11704710B5B24C206935 +:10AE700090F89411042916D190F864200123002140 +:10AE8000583002F08BF800B9FFDF206990F890107D +:10AE9000890703D4062180F8641004E0002180F8BB +:10AEA000881080F89411206990F86600800707D513 +:10AEB000FFF7C6FF206910F8661F21F0020101703C +:10AEC00010BD9D4910B5096991F864200A2A09D17D +:10AED00091F8CA20824205D1002081F8640081F8EF +:10AEE000880010BD91F86620130706D522F00800EF +:10AEF00081F86600BDE81040A2E7FF2801D0FFDF1F +:10AF000010BDBDE81040A7E710B58B4C05212069A6 +:10AF1000FEF708F8206990F84E10012903D0BDE82B +:10AF20001040FEF77EBB022180F84E1010BD10B518 +:10AF3000814C206910F8961F41F004010170A0694E +:10AF400002F041FF162806D1206990F864002028FD +:10AF500002D0262805D010BDA06902F038FFFEF708 +:10AF60003FFB2169002081F8640081F8880010BD52 +:10AF700070B5714C01230A21206990F86420583083 +:10AF800002F00CF810B3A06902F0C4FEA8B1256964 +:10AF9000A06902F0BBFE28872569A06902F0B2FE15 +:10AFA00068872569A06902F0B3FEA8872569A069B2 +:10AFB00002F0AAFEE887FEF7D5FC2169002081F89F +:10AFC000880081F86400BDE870409DE7A07840F0FB +:10AFD0000100A070BDE510B5574C01230021206988 +:10AFE00090F86520583001F0D9FF30B1FFF71FFF0E +:10AFF0002169102081F8650010BD20690123052119 +:10B0000090F86520583001F0C9FF08B1082000E031 +:10B010000120A07010BD70B5474C012300212069AC +:10B0200090F86520583001F0B9FF012588B1A0697A +:10B0300002F011FE2169A1F89601B1F85810FFF74E +:10B04000D8FE40B12069282180F8741080F8735030 +:10B050007FE5A5707DE52169A06901F5CC7102F05D +:10B06000F5FD21690B2081F8650072E510B5FEF74A +:10B0700016FFFEF714FE304CA079400708D5A078E3 +:10B0800030B9206990F86700072801D101202070AD +:10B09000FEF7CAF9A079C00609D5A07838B92069A9 +:10B0A00090F865100B2902D10C2180F86510E0782A +:10B0B00000070ED520690123052190F8652058303E +:10B0C00001F06CFF30B10820A0702169002081F8E8 +:10B0D000C00110BDBDE81040002000F093BB10B5CA +:10B0E000154C216991F86520F8B1102A06D0142A70 +:10B0F00007D0152A22D01B2A34D122E001210B20AF +:10B1000021E0FAF797FE0C281FD320690821F830B8 +:10B11000FAF794FE28B120690421C430FAF78EFEB4 +:10B1200000B9FFDF012104200DE010E043A8010079 +:10B1300083AA0100B9AA01000001002000F017F85D +:10B1400003E001210620FEF714FF012010BD212A93 +:10B1500008D191F87D0038B991F8AC0110B191F89F +:10B16000AD0108B1002010BD01211720EBE770B53B +:10B17000174C0025206990F87B1101290AD002297B +:10B1800025D190F88E10A9B1062180F8CE100121AA +:10B19000022017E090F8C011002918D100F1B00387 +:10B1A00000F1F001002200F5BE7001F071FE0121F6 +:10B1B000052007E090F89600400701D5112000E037 +:10B1C0000D200121FEF7D5FE206980F87B51C0E4F7 +:10B1D0000001002030B5FA4C05462078002818BF41 +:10B1E000FFDF257230BDF6490120C87170472DE997 +:10B1F000F14FF44E30464068044600F1580990F88B +:10B20000551001F0D2FF94F85510658E80B20829D0 +:10B210006CD001F0A8FF854238BF284600F0FF0837 +:10B22000DFF89CA3E848CAF824007768384697F806 +:10B230006AB07D8E97F8551001F0B7FF97F855105A +:10B2400080B2082956D001F08EFF854238BF2846CB +:10B25000BBF1000F1CBF001D80B2C0B297F85510A3 +:10B26000FBF770FB99F81200002847D009F158014C +:10B27000D54891E80E1000F5027585E80E10D9F852 +:10B280006810C0F82112D9F86C10C0F8251200F52A +:10B290008170FBF7BCFE307800280CBF0120002035 +:10B2A00080F00101C9480176D9E91412C0E90412FD +:10B2B000A0F58372DAF82410FBF7DBF994F8550057 +:10B2C000012808BF00220CD0022808BF012208D0A4 +:10B2D000042808BF032204D008281ABFFFDF002279 +:10B2E000022241460120FBF7DFF90DE0042101F0C5 +:10B2F0003AFF90E7042101F036FFA6E7DAF82400D0 +:10B30000FBF785FEFBF7FCF9009850B994F855005F +:10B3100094F8561010F00C0F08BF00219620FBF790 +:10B3200097FE94F8542001210020FBF779FF94F850 +:10B330002C00012808BFFBF743FF02208AF8000019 +:10B34000FCF74CFB002818BFFFDFBDE8F88F2DE9A4 +:10B35000F04FDFF870A28BB050469AF80020416899 +:10B360001438049091F85D0001F158050C464FF037 +:10B3700008080127AAF13406A0B3012800F00681CD +:10B38000022800F00781032818BFFFDF00F01081BA +:10B39000306A0423017821F008010170AA7908EAD3 +:10B3A000C202114321F004010170EA7903EA82022A +:10B3B000114321F01001017095F80590F06AF6F73D +:10B3C000DAFE8046FCF7BAFBB9F1020F00F000810B +:10B3D000B9F1010F00F00081B9F1030F00F0008115 +:10B3E00000F003B9FFE72B7B4FF002094FF0000B91 +:10B3F000242B1CBF95F80DC0BCF1240F07D01F2BC8 +:10B4000018BF202B2AD0BCF1220F4DD077E091F845 +:10B41000540092B191F89811002974D0082818BFEF +:10B42000042869D0082918BF042965D0012818BF4D +:10B43000012953D04FF0020065E091F8FA1000297D +:10B4400061D0082818BF042856D0082918BF04293D +:10B4500052D0012818BF012940D0EBE7BCF1220FE0 +:10B4600022D0002A4BD091F8540091F8AE1111F07F +:10B47000040F18BF41460CD0082818BF04283BD041 +:10B48000082918BF042937D0012818BF012925D061 +:10B49000D0E711F0010F18BF3946EDD111F0020FBE +:10B4A00018BF4946E8D12EE04AB391F8540091F80C +:10B4B000AE2191F8511002EA010111F0040F18BFFA +:10B4C00041460ED0082818BF042815D0082918BFF7 +:10B4D000042911D0012818BF0129ABD14FF0010078 +:10B4E00011E011F0010F18BF3946EBD111F0020F36 +:10B4F00018BF4946E6D106E04FF0080003E091F896 +:10B5000054000428F8D001460290204601F058FE6D +:10B5100080B2029901F027FE218E814238BF084691 +:10B52000ADF80C00A4F848000498FCF7E6FA60B106 +:10B53000B289316A42F48062B28172694FF48060EC +:10B54000904703206871EF7022E709AA03A9F06A07 +:10B55000F6F74CFD306210B195F8351021B1049822 +:10B56000FCF79FFA6F7113E79DF8241031B9A0F82A +:10B5700000B080F802B0012102F0F4FABDF80C101E +:10B58000306A02F026FC85F8059001E70498FCF784 +:10B5900088FAFDE6B4F84800ADF8080009AA02A947 +:10B5A000F06AF6F723FD3062002808BFFFDFEFE600 +:10B5B0000498FCF7A2FA002808BFFFDFE8E60000C5 +:10B5C0002401002058010020E00C0020E80E00209B +:10B5D00030EA080009D106E030EA080005D102E0AF +:10B5E000B8F1000F01D0012100E00021306A02789B +:10B5F00042EA01110170697C00291CBF69790129A7 +:10B600003DD005F15801FD4891E80E1000F5027893 +:10B6100088E80E10A96EC0F82112E96EC0F8251254 +:10B6200000F58170FBF7F3FC9AF8000000280CBFCE +:10B6300001200020F2490876D5E91202C1E904028E +:10B64000A1F5837101F58370326AFBF712F894F863 +:10B650005400012808BF00220CD0022808BF012294 +:10B6600008D0042808BF032204D008281ABFFFDF2F +:10B6700000220222FB210020FBF716F803E0FBF773 +:10B68000C6FCFBF73DF8012194F855200846FBF76E +:10B69000C7FD3771306A018831828078B0743770A5 +:10B6A000FCF7A5F9002818BFFFDF0BB0BDE8F08F4D +:10B6B0002DE9F047D34C8146DDF8208020781E46E6 +:10B6C00017460D4628B9002F1CBF002EB8F1000FF9 +:10B6D00000D1FFDFC4F81C80C4E90D95C4E90576EC +:10B6E0004FF00000E071A071E070A07020716071F7 +:10B6F000C54EA081E081307805F158072888F7F71A +:10B70000BDFAE0622888F7F7A7FA2063FBF73EF955 +:10B7100095F95700FBF7DFF905F11200FBF75AFC2A +:10B7200005F10E00FBF7DDF9307800280CBF03208F +:10B730000120FBF769FCB87EFBF7DBF9FBF75EFC49 +:10B740003078002804BFFF2095F8544019D0BF7C02 +:10B750006C8E95F85510284601F027FD95F8551088 +:10B7600080B208291FD001F0FEFC014620468C4221 +:10B7700028BF0846002F1CBF001D80B2C0B295F83C +:10B7800055402146FBF7DEF83078214680B1012094 +:10B79000FBF7A3FA7068D0F8E800FBF73BFCBDE8C4 +:10B7A000F047012023E5042101F0DDFC0146DDE73F +:10B7B0000020FBF792FABDE8F047C8E5924800B5D3 +:10B7C00001783438007819B1022818BFFFDF00BDB6 +:10B7D000012818BFFFDF00BD8A4810B50078022895 +:10B7E00018BFFFDFBDE8104000F034BA00F032BAF5 +:10B7F0008448007970478348C078704781490120A8 +:10B80000487170472DE9F04706007F487D4D40683C +:10B8100000F15804686A90F8019018BF012E03D116 +:10B82000296B09F069FB6870687800274FF0010800 +:10B83000A0B101283CD0022860D003281CBFFFDF44 +:10B84000BDE8F087012E08BFBDE8F087286BF6F74A +:10B8500087FE287ABDE8F047E7F75EBB012E14D0DB +:10B86000A86A002808BFFFDF6889C21CD5E9091053 +:10B8700009F084FEA86A686201224946286BF6F73F +:10B88000EBFC022E08BFBDE8F087D4E91401401C90 +:10B8900041F10001C4E91401E079012801D1E77107 +:10B8A00001E084F80780287ABDE8F047E7F734BB69 +:10B8B000012E14D0A86A002808BFFFDF6889C21CC7 +:10B8C000D5E9091009F05AFEA86A686200224946C3 +:10B8D000286BF6F7C1FC022E08BFBDE8F087D4E95B +:10B8E0001410491C40F10000C4E91410E07901284B +:10B8F0000CBFE77184F80780BDE8F087012E06D001 +:10B90000286BF6F72DFE022E08BFBDE8F087D4E9BC +:10B910001410491C40F10000C4E91410E07901281A +:10B92000BFD1BCE770B5384E3046A6F1340440684C +:10B9300000F158052078012818BFFFDFA87868B10A +:10B940000021A970A289042042F00402A281626948 +:10B950009047307800281CBF01202871216A0322FB +:10B96000087832EA000009D1A28912F4806F05D06C +:10B9700042F00202A2816269022090470121002068 +:10B9800000F087F918B1BDE8704000F063B9BDE878 +:10B99000704000202BE42DE9F14F1B4E002730466C +:10B9A000A6F134054068317800F1580A2878B84685 +:10B9B000022818BFFFDFE88940F40070E881716851 +:10B9C0003078FF2091F85410FAF7BCFF0098002857 +:10B9D0009AF8120000F00681FAF7B7FEFAF7A5FE12 +:10B9E0004FF00109E0B99AF81200C8B1686A4178CD +:10B9F000B1B10078C0F3C00008E00000E00C002006 +:10BA0000E80E002024010020580100209AF80710B9 +:10BA1000884205D185F80290BDE8F84F00F01AB9C8 +:10BA2000686A41786981002908BFAF6203D0286B3A +:10BA3000F6F7CCFBA862E88940F02000E881EF70BF +:10BA40003078706800F15804834690F82C00012883 +:10BA50001AD1FBF7ABFB2146584601F05AFA98B1D0 +:10BA60003078002870680CBF00F58E7000F5F97012 +:10BA7000BBF800104180217A0171617A417180F830 +:10BA80000090287AE7F748FA686A9AF80610007872 +:10BA9000C0F3800088423BD03078706800F15804D1 +:10BAA00090F85D0000282FD002284BD067713078C5 +:10BAB00000281CBF2079002809D02771AA8939469F +:10BAC00042F01002AA816A694FF010009047E078B6 +:10BAD000A0B1E770FCF720F8002808BFFFDF0820BE +:10BAE000AA89002142F00802AA816A699047D4E934 +:10BAF0001202411C42F10000C4E91210A079012891 +:10BB00000CBFA77184F80690E88940F48070E88142 +:10BB1000696A9AF807300878C0F3C0029A424ED199 +:10BB20003278726800F0030002F15804012818BF4F +:10BB300002282DD003281CBFA87940F0040012D0A1 +:10BB4000A8713CE0E86AF6F77DFA002808BFFFDF3D +:10BB5000D4E91202411C42F10000C4E91210287A13 +:10BB6000E7F7DAF9A2E784F80290EA89484642F456 +:10BB70000062EA81AA8942F00102AA816A699047BB +:10BB8000E079012801D1E77119E084F8079016E007 +:10BB9000487818B3E98941F40061E981A96A71B173 +:10BBA000FB2884BFA87940F01000C9D8E8790028A4 +:10BBB00008BFC84603D080206A6900219047012051 +:10BBC000009900F066F8B0B1B8F1000F1CBF00207A +:10BBD000FFF718FEBDE8F84F00F03CB8E079012807 +:10BBE000D3D1D0E7002818BFFAF7E7FDE88940F085 +:10BBF0004000E881E3E7B8F1000F1CBF0120FFF728 +:10BC000001FEFFF7A4FBB8F1000F08BFBDE8F88FF5 +:10BC10000220BDE8F84FF5E570B50D4606463D48F3 +:10BC20003C4900784C6850B1FAF724FE034694F87A +:10BC3000542029463046BDE87040FDF76DBAFAF74A +:10BC400019FE034694F8542029463046BDE870405A +:10BC500006F091B92F4910B54C68FBF786FAFBF74F +:10BC600065FAFBF73DF9FBF7BBF9FAF749FD94F8E4 +:10BC70002C00012808BFFBF799FA274C00216269C4 +:10BC8000E0899047E269A179A07890470020207070 +:10BC900010BD70B5204C0546002908BF012D06D106 +:10BCA000E07800F10100C0B2E07001282ED8A1694F +:10BCB00028468847002829D06179184839B1012DD4 +:10BCC00001BF41780029017811F0100F1ED0A17931 +:10BCD000E1B910490978002908BF012D01D091B1BF +:10BCE0008DB90F49097811F0100F04BF007810F0DA +:10BCF000100F0BD0A08948B9A06A20B9608910B193 +:10BD000011F0100F02D04FF0000070BD4FF0010095 +:10BD100070BD00005801002024010020E00C00202C +:10BD200034010020FE498A78824286BF084490F898 +:10BD300043010020704710B540F2D311F84809F0D4 +:10BD40009CFCFF220821F74809F08FFCF6480021EF +:10BD5000417081704FF46171818010BD2DE9F04117 +:10BD60000E46054600F0ADFBED4C102816D004EB56 +:10BD7000C00191F85A0110F0010F1CBF0120BDE86D +:10BD8000F081607808283CBF012081F85A011CD25C +:10BD90006078401C60700120BDE8F0816078082860 +:10BDA00013D222780127501C207004EBC20830689F +:10BDB000C8F85401B088A8F85801102A28BFFFDF3E +:10BDC00088F8535188F85A71E2E70020BDE8F08105 +:10BDD000D54988707047D4488078704770B4D0488F +:10BDE00000250178491E4BB2002B46DB00EBC30156 +:10BDF00091F85A1111F0010F3BD04278D9B2521E7E +:10BE0000427000EBC10282F85A5190F802C0002241 +:10BE1000BCF1000F0BD9841894F803618E4202D153 +:10BE2000102A26D103E0521CD2B29445F3D80278EE +:10BE3000521ED2B202708A421BD000EBC20200EB4B +:10BE4000C10CD2F85341CCF85341D2F85721CCF869 +:10BE50005721847890F800C00022002C09D9861858 +:10BE600096F8036166450CD1102A1CBF024482F883 +:10BE70000311591E4BB2002BB8DAAB48857070BC69 +:10BE80007047521CD2B29442E9D8F2E7A4498A78AA +:10BE9000824286BF01EB0010C01C002070472DE9D4 +:10BEA000F04101261F4690463446002500F009FB6C +:10BEB00010282AD09A494FF0000C01EBC00292F8EA +:10BEC0005A2102F001058A78002A1ED901EB0C03E1 +:10BED00093F8033183421FD1BCF1100F15D0002F0E +:10BEE00018BF87F800C0887860450ED901EB0C10A8 +:10BEF00010F1030F09D001EB0C0090F84B4190F8C2 +:10BF00003B0101280CBF0126002648EA050046EA4D +:10BF100004010840BDE8F0810CF1010303F0FF0CBF +:10BF20006245D3D8F1E72DE9F05F1F4690460E46F3 +:10BF3000814600F0C6FA7A4D044610283CD00146EE +:10BF4000AB780020002B0ED92A1892F803218A42E0 +:10BF500005D110281CBF1220BDE8F09F03E0401C53 +:10BF6000C0B28342F0D8082B3FD2102C27D0AE7835 +:10BF70001022701CA87005EB061909F10300414658 +:10BF800000F06CFF09F183001022394600F066FFD3 +:10BF90001021384600F03FFF3544102185F8430159 +:10BFA000404600F038FF85F84B0185F8034100203A +:10BFB00085F83B01BDE8F09FAB78082B15D22C78B3 +:10BFC000CA46601C287005EBC4093068C9F85401E2 +:10BFD000B0884FF0000BA9F85801102C28BFFFDFE4 +:10BFE00089F853A189F85AB1C1E70720BDE8F09F4D +:10BFF00070B44B488178491E4BB2002BBCBF70BC5B +:10C00000704700BF817803F0FF0C491ECAB28270EE +:10C0100050FA83F191F8031194453ED000EB0215DC +:10C0200000EB0C14D5F80360C4F80360D5F8076082 +:10C03000C4F80760D5F80B60C4F80B60D5F80F6042 +:10C04000C4F80F60D5F88360C4F88360D5F88760C2 +:10C05000C4F88760D5F88B60C4F88B60D5F88F5032 +:10C06000C4F88F50851800EB0C0402EB420295F8DF +:10C0700003610CEB4C0C00EB420284F8036100EB13 +:10C080004C0CD2F80B61CCF80B61B2F80F21ACF874 +:10C090000F2195F83B2184F83B2100EBC10292F877 +:10C0A0005A2112F0010F33D190F802C00022BCF1E6 +:10C0B000000F0BD9841894F803518D4202D1102A35 +:10C0C00026D103E0521CD2B29445F3D80278521E16 +:10C0D000D2B202708A421BD000EBC20200EBC10C4C +:10C0E000D2F85341CCF85341D2F85721CCF857211C +:10C0F000847890F800C00022002C09D9851895F8A2 +:10C100000351654512D1102A1CBF024482F8031165 +:10C11000591E4BB2002BBFF675AF70BC70470000C4 +:10C12000100F00206C01002060010020521CD2B2D0 +:10C130009442E3D8ECE7FE4948707047FC484078E9 +:10C14000704738B14AF2B811884203D8F84988805C +:10C150000120704700207047F5488088704710B56F +:10C1600000F0AFF9102814D0F24A0146002092F8EE +:10C1700002C0BCF1000F0CD9131893F803318B42A5 +:10C1800003D1102818BF10BD03E0401CC0B2844585 +:10C19000F2D8082010BDE7498A78824286BF01EBB9 +:10C1A0000010833000207047E24B93F802C08445B2 +:10C1B0009CBF00207047184490F8030103EBC000B7 +:10C1C00090F853310B70D0F854111160B0F8580149 +:10C1D000908001207047D74A114491F80321D44937 +:10C1E0000A700268C1F8062080884881704770B5DF +:10C1F00016460C460546FAF7CEFFFAF796F9CC48F4 +:10C20000407868B1CB48817851B12A19002E0CBF13 +:10C210008330C01CFAF763F9FAF7AAF9012070BD60 +:10C22000002070BD10B5FAF7D1F9002804BFFF2037 +:10C2300010BDBDE81040FAF7EFB9FAF7C7B9BD492C +:10C240008A7882429CBF00207047084490F803011E +:10C2500001EBC00090F85A0100F0010070472DE991 +:10C26000F047B44E00273D46307800288CBFDFF8F9 +:10C27000C882BDE8F0870024B078002808D93119B9 +:10C2800091F80321AA4204D0611CCCB2A042F6D896 +:10C290001024A04286BF06EB0410C01C002006EB51 +:10C2A000C50999F85A1111F0010F16D050B1102C90 +:10C2B00004D0311991F83B11012903D0102100F06D +:10C2C000AAFD50B108F8074038467B1C99F8532165 +:10C2D00009F5AA71DFB2FAF7D6FB681CC5B230784F +:10C2E000A842C8D8BDE8F0872DE9F041914C00265E +:10C2F0003546A07800288CBF8F4FBDE8F0816119CA +:10C30000C0B291F80381A84286BF04EB0510C01C9F +:10C31000002091F83B11012903D0102100F07BFD92 +:10C3200058B104EBC800BD5590F8532100F5AA712F +:10C330003046731CDEB2FAF7A6FB681CC5B2A078C3 +:10C34000A842DCD8BDE8F08101447A4810B500EB82 +:10C3500002100A4601218330FAF7C1F8BDE8104007 +:10C36000FAF706B90A46724910B5497841B1714BDE +:10C37000997829B10244D81CFAF7B1F8012010BD10 +:10C38000002010BD6B4A01EB410102EB4101026844 +:10C39000C1F80B218088A1F80F0170472DE9F04109 +:10C3A000644D07460024A878002898BFBDE8F081B6 +:10C3B000C0B2A04217D905EB041010F1830612D0C9 +:10C3C0001021304600F027FD68B904EB440005EB6E +:10C3D000400808F20B113A463046FBF72CFCB8F83F +:10C3E0000F01A8F80F01601CC4B2A878A042DFD8E2 +:10C3F000BDE8F08101461022504800F02FBD4F48A3 +:10C4000070474C498A78824203D90A1892F843212E +:10C410000AB10020704700EB400001EB400000F241 +:10C420000B10704743498A78824206D9084490F835 +:10C430003B01002804BF01207047002070472DE910 +:10C44000F0410E46074615460621304600F0E3FC53 +:10C45000384C98B1A17871B104F59D7011F0010FBD +:10C4600018BF00F8015FA178490804D0457000F8B2 +:10C47000025F491EFAD10120BDE8F08138463146FD +:10C4800000F01FF8102819D0A3780021002B15D92F +:10C49000621892F8032182420BD1102918BF082993 +:10C4A0000CD004EB010080F83B514FF00100BDE8D7 +:10C4B000F08101F10101C9B28B42E9D80020BDE849 +:10C4C000F0812DE9F0411B4D0646002428780F46E7 +:10C4D000002811D905EBC40090F85311B14206D1E0 +:10C4E0000622394600F5AA7009F01CF838B1601C24 +:10C4F000C4B22878A042EDD81020BDE8F0812046D3 +:10C50000BDE8F0810B4910B44A7801EBC003521E1C +:10C510004A70002283F85A2191F802C0BCF1000F42 +:10C5200016D98B1893F8034184420DD1102A07E0E5 +:10C5300060010020100F00206C010020E31000209B +:10C540001CBF10BC704703E0521CD2B29445E8D81F +:10C550000A78521ED2B20A7082421BD001EBC2028C +:10C5600001EBC003D2F853C1C3F853C1D2F857212D +:10C57000C3F857218C7891F800C00022002C09D90B +:10C580008B1893F80331634506D1102A1CBF114460 +:10C5900081F8030110BC7047521CD2B29442EFD80C +:10C5A00010BC704770B449490D188A78521ED3B236 +:10C5B0008B7095F8032198423DD001EB001401EBFC +:10C5C000031C00EB4000DCF80360C4F80360DCF8F7 +:10C5D0000760C4F80760DCF80B60C4F80B60DCF897 +:10C5E0000F60C4F80F60DCF88360C4F88360DCF887 +:10C5F0008760C4F88760DCF88B60C4F88B60DCF877 +:10C600008FC0C4F88FC001EB030C03EB43039CF80D +:10C61000034101EB430385F8034101EB4000D3F8EC +:10C620000B41C0F80B41B3F80F31A0F80F319CF863 +:10C630003B0185F83B0101EBC20090F85A0110F074 +:10C64000010F1CBF70BC704700208C78002C0DD9E6 +:10C650000B1893F803C1944504D110281CBF70BC7B +:10C66000704703E0401CC0B28442F1D80878401EF5 +:10C67000C0B20870904204BF70BC704701EBC203A7 +:10C6800001EBC000D0F853C1C3F853C1D0F8570133 +:10C69000C3F857018C780B780020002C9CBF70BC2D +:10C6A000704700BF01EB000C9CF803C19C4506D10C +:10C6B00010281CBF084480F8032170BC7047401C40 +:10C6C000C0B28442EED870BC70470000100F00204A +:10C6D00010B50A7B02F01F020A73002202768B1843 +:10C6E00093F808C00CF001034FEA5C0C0CF0010455 +:10C6F00023444FEA5C0C0CF0010423444FEA5C0C29 +:10C700000CF001041C444FEA5C0303F0010CA44448 +:10C710005B0803F00104A4445B0803F00104A44493 +:10C720000CEB530300EB020C521C8CF8133090F806 +:10C7300018C0D2B263440376052AD0D3D8B22528D4 +:10C7400088BFFFDF10BD0023C383428401EBC20218 +:10C75000521EB2FBF1F10184704770B5002504460A +:10C7600003290DD04FF4FA4200297FD001297CD053 +:10C77000022918BF70BD0146BDE870405830A7E7D8 +:10C7800004F158068021304608F099FFB571F57123 +:10C7900035737573F573357475717576B5762120BB +:10C7A00086F83E00492086F83F00FE2086F8740097 +:10C7B00084F82C502584012084F8540084F8550016 +:10C7C000282184F856101B21218761874FF4A4711A +:10C7D000E187A1871B21218661864FF4A471E18640 +:10C7E000A1861B21A4F84010A4F844104FF4A471B2 +:10C7F000A4F84610A4F842101B21A4F84A10A4F88B +:10C800004C10A4F8481060734FF448606080A4F89E +:10C81000D850A4F8DA50A4F8DC50A4F8DE50A4F8FC +:10C82000E050A4F8E25084F8E55084F8E750A4F80A +:10C83000EE5084F8EC50A4F80051A4F8025184F8AA +:10C84000A25184F8A35184F8AC5184F8AD5184F816 +:10C85000705184F8785184F87B5184F89451C4F86D +:10C860008C51C4F8905170BD00E041E0A4F8EE5046 +:10C8700084F8E6506088FE490144B1FBF0F1A4F869 +:10C8800078104BF68031A4F87A10E388A4F87E5033 +:10C89000B4F882C0DB000CFB00FCB3FBF0F39CFBA4 +:10C8A000F0FC5B1CA4F882C09BB203FB00FC04F10B +:10C8B0005801A4F88030BCF5C84FC4BF5B1E0B857F +:10C8C000B2FBF0F2521CCA8500F5802202F5EE326E +:10C8D000531EB3FBF0F20A84CB8B03FB00F2B2FBD6 +:10C8E000F0F0C883214604F15800BDE87040EFE63F +:10C8F000B4F89C11B4F8A031B4F802C004F15800A7 +:10C90000A4F87E50B4F88240DB0004FB0CF4B3FBC7 +:10C91000F1F394FBF1F45B1C44859BB203FB01F43F +:10C920000385B4F5C84FC4BF5B1E0385B2FBF1F2AB +:10C93000521CC285428C01EBC202521EB2FBF1F2C4 +:10C940000284C28B02FB0CF2B2FBF1F1C18370BD19 +:10C9500070B50025044603290DD04FF4FA42002992 +:10C9600063D001297ED0022918BF70BD0146BDE801 +:10C9700070405830ACE604F158068021304608F08B +:10C980009EFEB571F57135737573F57335747571F8 +:10C990007576B576212086F83E00492086F83F005E +:10C9A000FE2086F8740084F82C502584012084F839 +:10C9B000540084F85500282184F856101B21218743 +:10C9C00061874FF4A471E187A1871B2121866186CD +:10C9D0004FF4A471E186A1861B21A4F84010A4F8AD +:10C9E00044104FF4A471A4F84610A4F842101B217F +:10C9F000A4F84A10A4F84C10A4F848106073A4F8E6 +:10CA0000E050202084F8E20084F8D850C4F8DC50CC +:10CA100084F80C5184F80D5184F8165184F817519C +:10CA200084F8FC5084F8085170BD60889049014436 +:10CA3000B1FBF0F1A4F878104BF68031A4F87A102D +:10CA4000E388A4F87E50B4F882C0DB000CFB00FC45 +:10CA50009CFBF0FCB3FBF0F304F15801A4F882C096 +:10CA60005B1C00E021E09BB203FB00FCA4F88030DB +:10CA7000BCF5C84FC4BF5B1E0B85B2FBF0F2521C65 +:10CA8000CA8500F5802202F5EE32531EB3FBF0F2A8 +:10CA90000A84CB8B03FB00F2B2FBF0F0C883214683 +:10CAA00004F15800BDE8704012E6D4F80031B4F843 +:10CAB00002C004F158005989DB89A4F87E50B4F80B +:10CAC0008240DB0004FB0CF4B3FBF1F394FBF1F4C4 +:10CAD0005B1C44859BB203FB01F40385B4F5C84F8E +:10CAE000C4BF5B1E0385B2FBF1F2521CC285428CAF +:10CAF00001EBC202521EB2FBF1F20284C28B02FBB6 +:10CB00000CF2B2FBF1F1C18370BD2DE9F003047E9C +:10CB10000CB1252C03D9BDE8F00312207047002A80 +:10CB200002BF0020BDE8F003704791F80DC01F263A +:10CB30000123504D4FF00008BCF1000F74D0BCF140 +:10CB4000010F1EBF1F20BDE8F0037047B0F800C002 +:10CB50000A7C8F7B91F80F907A404F7C87EA090717 +:10CB600042EA072282EA0C0C5FF000070CF0FF0992 +:10CB70004FEA1C2C99FAA9F99CFAACFC4FEA196906 +:10CB80004FEA1C6C49EA0C2C0CEB0C1C7F1C9444E7 +:10CB9000FFB21FFA8CFC032FE8D38CEA020C354F4E +:10CBA0000022ECFB057212096FF0240502FB05C29E +:10CBB000D2B201EBD207427602F007053F7A03FAC0 +:10CBC00005F52F4218BF82767ED104FB0CF2120CC1 +:10CBD000521CD2B25FF0000400EB040C9CF813C0AE +:10CBE00094453CBFA2EB0C02D2B212D30D194FF008 +:10CBF000000C2D7A03FA0CF73D421CBF521ED2B234 +:10CC0000002A71D00CF1010C0CF0FF0CBCF1080FE4 +:10CC1000F0D304F1010C0CF0FF04052CDCD33046FA +:10CC2000BDE8F0037047FFE790F819C00C7E474657 +:10CC300004FB02C20F4C4FF0000CE2FB054C4FEA24 +:10CC40001C1C6FF024040CFB0422D2B201EBD204B2 +:10CC5000427602F0070C247A03FA0CFC14EA0C0F5B +:10CC60001FBF82764046BDE8F003704704E0000035 +:10CC7000FFDB050053E4B36E90F818C0B2FBFCF480 +:10CC80000CFB1422521CD2B25FF0000400EB040C27 +:10CC90009CF813C094453CBFA2EB0C02D2B212D355 +:10CCA0000D194FF0000C2D7A03FA0CF815EA080F55 +:10CCB0001CBF521ED2B27AB10CF1010C0CF0FF0C69 +:10CCC000BCF1080FF0D300E011E004F1010C0CF00E +:10CCD000FF04052CDAD3A2E70CEBC40181763846B9 +:10CCE000BDE8F0037047FFE70CEBC40181764046D6 +:10CCF000BDE8F0037047FD4A016812681140FC4A24 +:10CD0000126811430160704730B4FA49F74B0024B0 +:10CD10004FF0010C0A78521CD2B20A70202A08BFC8 +:10CD20000C700D781A680CFA05F52A42F2D00978D1 +:10CD300002680CFA01F15140016030BC704770B4D8 +:10CD40006FF01F02010C02EA90251F23A1F5AA40F3 +:10CD500054381CBFA1F5AA40B0F1550009D0A1F587 +:10CD60002850AA381EBFA1F52A40B0F1AA00012020 +:10CD700000D100204FF0000C624664468CEA0106A8 +:10CD8000F6431643B6F1FF3F11D005F001064FEA16 +:10CD90005C0C4CEAC63C03F0010652086D085B08C7 +:10CDA000641C42EAC632162CE8D370BC704770BCD3 +:10CDB00000207047017931F01F0113BF00200022CD +:10CDC0001146704710B4435C491C03F0010C5B082A +:10CDD00003F00104A4445B0803F00104A4445B08CD +:10CDE00003F00104A4445B0803F00104A4445B08BD +:10CDF00003F001045B08A44403F00104A4440CEB19 +:10CE000053031A44D2B20529DDDB012A8CBF01206D +:10CE1000002010BC704730B40022A1F1010CBCF11D +:10CE2000000F11DD431E11F0010F08BF13F8012F91 +:10CE30005C785FEA6C0C07D013F8025F22435C78E1 +:10CE40002A43BCF1010CF7D1491E5CBF405C024390 +:10CE5000002A0CBF0120002030BC7047002A08BF08 +:10CE600070471144401E12F0010F03D011F8013D2C +:10CE700000F8013F520808BF704700BF11F8013C9D +:10CE8000437011F8023D00F8023F521EF6D1704780 +:10CE900070B58CB000F110041D4616460DF1FF3C34 +:10CEA0005FF0080014F8012C8CF8012014F8022D12 +:10CEB0000CF8022F401EF5D101F1100C6C460DF15B +:10CEC0000F0108201CF8012C4A701CF8022D01F8F3 +:10CED000022F401EF6D1204607F0FAF97EB16A1EF5 +:10CEE00004F130005FF0080110F8013C537010F8B5 +:10CEF000023D02F8023F491EF6D10CB070BD089801 +:10CF00002860099868600A98A8600B98E8600CB0DF +:10CF100070BD38B505460C466846FAF760F900283A +:10CF200008BF38BD9DF900202272A07E607294F97E +:10CF30000A100020511A48BF494295F82D308B4203 +:10CF4000C8BF38BDFF2B08BF38BDE17A491CC9B244 +:10CF5000E17295F82E30994203D8A17A7F2918BF43 +:10CF600038BDA2720020E072012038BD0C2818BF25 +:10CF70000B2810D00D2818BF1F280CD0202818BF50 +:10CF8000212808D0222818BF232804D024281EBF17 +:10CF90002628002070474FF0010070470C2963D20B +:10CFA000DFE801F006090E13161B323C415C484EC7 +:10CFB000002A5BD058E0072A18BF082A56D053E051 +:10CFC0000C2A18BF0B2A51D04EE00D2A4ED04BE050 +:10CFD000A2F10F000C2849D946E023B1A2F11000BC +:10CFE0000B2843D940E0122A18BF112A3ED090F8EE +:10CFF000360020B1122A37D31A2A37D934E0162A3C +:10D0000032D31A2A32D92FE0A2F10F0103292DD9E8 +:10D0100090F8360008B31B2A28D925E0002B08BF5A +:10D02000042A21D122E013B1062A1FD01CE0012AD4 +:10D030001AD11BE01C2A1CBF1D2A1E2A16D013E081 +:10D040001F2A18BF202A11D0212A18BF222A0DD04A +:10D05000232A1CBF242A262A08D005E013B10E2A51 +:10D0600004D001E0052A01D000207047012070475C +:10D070002DE9F0410D4604468668F7F7CCFF58B914 +:10D08000F7F7FAFD40F23471F7F7F7FAA06020469F +:10D09000F7F7C1FF0028F3D095B13046A168F8F743 +:10D0A00004FB00280CDD2844401EB0FBF5F707FB0D +:10D0B00005F13046F7F7E1FAA0603846BDE8F081A7 +:10D0C0000020BDE8F08170B50446904228BF70BDD5 +:10D0D000101B642810D325188D4205D8F8F719FBCA +:10D0E00000281CBF284670BD204670BD785C020039 +:10D0F0007C5C0200740100206420ECE710B4B1F8FD +:10D1000002C0A0F840C0B1F806C0A0F844C0B1F811 +:10D1100004C090F85440098914F00C0F15D000BFDA +:10D12000BCF5296F98BF4FF4296C90F8554014F066 +:10D130000C0F11D0B1F5296F98BF4FF42961A0F8F9 +:10D1400042C0A0F8461010BC7047002B1CBF1478DA +:10D1500014F00C0FE4D1E8E7002B1CBF527812F05A +:10D160000C0FE7D1EBE711F00C0F13D001F0040125 +:10D1700000290DBF4022102296214FF4167101F5AF +:10D18000BC71A0EB010388428CBF93FBF2F000203E +:10D1900080B27047022919BF6FF00D0101EBD0007A +:10D1A0006FF00E0101EB9000F2E7C08E11F00C0F52 +:10D1B00008BF7047B0F5296F38BF4FF4296070473A +:10D1C0000246808E11F00C0F08BF704792F8553060 +:10D1D000D18E13F00C0F04D0B1F5296F38BF4FF486 +:10D1E0002961538840F2E24C03FB0CF3528E4FF45A +:10D1F000747C0CEB821C8C459CBF910101F5747111 +:10D20000591AA1F59671884228BF0846B0F5296FD2 +:10D2100038BF4FF429607047084418449830002AFA +:10D2200014BF0421002108447047F0B4002A14BF41 +:10D2300008220122002B14BF0824012412F00C0F35 +:10D240008B8ECA8E25D091F85550944615F00C0F50 +:10D2500004D0BCF5296F38BF4FF4296C4D8840F2DB +:10D26000E2466E434D8E4FF4747707EB85176745A2 +:10D270009CBF4FEA851C0CF5747CA6EB0C0CACF53E +:10D28000967C634528BF6346B3F5296F38BF4FF4DA +:10D29000296314F00C0F04D0B2F5296F38BF4FF496 +:10D2A00029621FFA83FC00280CBF0123002391F898 +:10D2B000560014F00C0F08BF00200CEB02010844CC +:10D2C0009830002B14BF042100210844F0BC7047A3 +:10D2D0002DE9F00391F854200B8E12F00C0F4FF44F +:10D2E00074771CBF07EB83139CB255D012F00C0F60 +:10D2F0008B8ECA8E4D8E91F855C021D016461CF0EB +:10D300000C0F04D0B6F5296F38BF4FF42966B1F879 +:10D31000028040F2E24908FB09F807EB8519B145A4 +:10D3200002D8AE0106F57476A8EB0606A6F5967649 +:10D33000B34228BF3346B3F5296F38BF4FF4296392 +:10D34000A34228BF23469CB21CF00C0F1CBF07EB66 +:10D3500085139BB228D000BF1CF00C0F04D0B2F58F +:10D36000296F38BF4FF429629A4228BF1A46002815 +:10D370000CBF0123002391F856001CF00C0F08BFCE +:10D380000020A11808449830002B14BF042100216C +:10D390000844BDE8F0037047022A07BF9B003C33F6 +:10D3A000DB0070339CB2A1E7BCF1020F07BFAB00FA +:10D3B0003C33EB0070339BB2CEE710F0010F1CBF83 +:10D3C0000120704710F0020F1CBF0220704710F0C0 +:10D3D000040018BF082070472DE9F047044617469F +:10D3E00089464FF00108084600F0C5FC054648464E +:10D3F00000F0C5FC10F0010F18BF012625D000BFBA +:10D4000015F0010F18BF01232AD000BF56EA03010F +:10D4100008BF4FF0000810F0070F08BF002615F0F6 +:10D42000070F08BF002394F85400B0420CBF00203F +:10D430003046387094F85510994208BF00237B702D +:10D44000002808BF002B25D115E010F0020F18BFEF +:10D450000226D5D110F0040F14BF08260026CFE70E +:10D4600015F0020F18BF0223D0D115F0040F14BF1E +:10D4700008230023CAE7484600F087FCB4F8581098 +:10D48000401A00B247F6FE71884201DC002801DC38 +:10D490004FF0000816B1082E0CD018E094F8540094 +:10D4A000012818BF022812D004281EBF0828FFDF59 +:10D4B000032D0CD194F8AC0148B1B4F8B0010128A7 +:10D4C00094F8540006D0082801D00820387040464F +:10D4D000BDE8F087042818BF0420F7D1F5E701283C +:10D4E00014BF0228704710F00C0018BF04207047CA +:10D4F00038B4CBB2C1F3072CC1B2C0F30724012B5F +:10D5000007D0022B09D0042B08BFBCF1040F2DD08B +:10D5100006E0BCF1010F03D128E0BCF1020F25D0D9 +:10D52000012906D0022907D0042908BF042C1DD0E8 +:10D5300004E0012C02D119E0022C17D001EA0C0101 +:10D5400061F3070204EA030161F30F22D1B211F083 +:10D55000020F18BF022310D0C2F307218DF800304C +:10D5600011F0020F18BF02211BD111E0214003EA84 +:10D570000C03194061F30702E6E711F0010F18BF31 +:10D580000123E9D111F0040F14BF08230023E3E7BE +:10D5900011F0010F18BF012103D111F0040118BFD0 +:10D5A00008218DF80110082B01BF000C0128042070 +:10D5B0008DF80000BDF8000038BC70474FF0000C3B +:10D5C000082902D0042909D011E001280FD1042034 +:10D5D000907082F803C0138001207047012806D0A4 +:10D5E0000820907082F803C013800120704700204B +:10D5F0007047162A10D12A220C2818BF0D280FD0E8 +:10D600004FF0230C1F280DD031B10878012818BF26 +:10D61000002805D0162805D000207047012070474B +:10D620001A70FBE783F800C0F8E7012908D0022947 +:10D630000BD0042912BF082940F6A660704707E006 +:10D64000002804BF40F2E240704740F6C410704723 +:10D6500000B5FFDF40F2E24000BD000040787047B7 +:10D6600030B50546007801F00F0220F00F0010439E +:10D670002870092912D2DFE801F00507050705091E +:10D68000050B0F0006240BE00C2409E0222407E020 +:10D6900001240020E87003E00E2401E00024FFDFF5 +:10D6A0006C7030BD007800F00F0070470A68C0F859 +:10D6B00003208988A0F807107047D0F803200A607B +:10D6C000B0F80700888070470A68C0F80920898888 +:10D6D000A0F80D107047D0F809200A60B0F80D00CE +:10D6E000888070470278402322F0400203EA8111CB +:10D6F0001143017070470078C0F3801070470278C2 +:10D70000802322F0800203EAC111114301707047A7 +:10D710000078C009704770B514460E4605461F2AAA +:10D7200088BFFFDF2246314605F1090007F026FFDA +:10D73000A01D687070BD70B544780E460546062C75 +:10D7400038BFFFDFA01F84B21F2C88BF1F242246D2 +:10D7500005F10901304607F011FF204670BD70B594 +:10D7600014460E4605461F2A88BFFFDF2246314673 +:10D7700005F1090007F002FFA01D687070BD09687F +:10D78000C0F80F1070470A88A0F8132089784175F7 +:10D79000704790F8242001F01F0122F01F0211436E +:10D7A00080F824107047072988BF072190F82420AB +:10D7B000E02322F0E00203EA4111114380F8241033 +:10D7C00070471F3008F08FB810B5044600F009FB11 +:10D7D000002818BF204410BDC17811F03F0F1BBFB7 +:10D7E000027912F0010F0022012211F03F0F1BBF3E +:10D7F000037913F0020F002301231A4402EB4202C3 +:10D80000530011F03F0F1BBF027912F0080F0022E6 +:10D81000012203EB420311F03F0F1BBF027912F00C +:10D82000040F00220122134411F03F0F1BBF0279A5 +:10D8300012F0200F0022012202EBC20203EB42038E +:10D8400011F03F0F1BBF027912F0100F00220122CE +:10D8500002EB42021A4411F03F0F1BBF007910F097 +:10D86000400F00200120104410F0FF0014BF0121E0 +:10D8700000210844C0B2704770B50278417802F0C8 +:10D880000F02082A4DD2DFE802F004080B4C4C4C82 +:10D890000F14881F1F280AD943E00C2907D040E045 +:10D8A000881F1F2803D93CE0881F1F2839D8012072 +:10D8B00070BD4A1EFE2A34D88446C07800258209ED +:10D8C000032A09D000F03F04601C884204D8604657 +:10D8D000FFF782FFA04201D9284670BD9CF80300E3 +:10D8E0004FF0010610F03F0F1EBF1CF1040000783E +:10D8F00010F0100F13D064460421604600F071FA56 +:10D90000002818BF14EB0000E6D0017801F03F01B9 +:10D910002529E1D280780221B1EB501FDCD33046BB +:10D9200070BD002070BD70B50178012501F00F01B8 +:10D93000002404290AD007290DD008291CBF002083 +:10D9400070BD40780E2836D0204670BD4078801FCC +:10D950001F2830D9F8E7844640789CF803108A09DC +:10D96000032AF1D001F03F06711C8142ECD86046D9 +:10D97000FFF732FFB042E7D89CF8030010F03F0FEA +:10D980001EBF1CF10400007810F0100F13D0664683 +:10D990000421604600F025FA002818BF16EB0000AD +:10D9A000D2D0017801F03F012529CDD28078022123 +:10D9B000B1EB501FC8D3284670BD10B4017801F0F8 +:10D9C0000F01032920D0052921D14478B0F819107E +:10D9D000B0F81BC0B0F81730827D222C17D1062971 +:10D9E00015D3B1F5486F98BFBCF5FA7F0FD272B16D +:10D9F000082A98BF8A420AD28B429CBFB0F81D0009 +:10DA0000B0F5486F03D805E040780C2802D010BC70 +:10DA10000020704710BC012070472DE9F0411F46DF +:10DA200014460D00064608BFFFDF2146304600F0D1 +:10DA3000D8F9040008BFFFDF30193A462946BDE88F +:10DA4000F04107F09BBDC07800F03F007047C02256 +:10DA500002EA8111C27802F03F021143C17070479F +:10DA6000C07880097047C9B201F00102C1F34003D8 +:10DA70001A4402EB4202C1F3800303EB4202C1F3FA +:10DA8000C00302EB4302C1F3001303EB43031A4448 +:10DA9000C1F3401303EBC30302EB4302C1F3801352 +:10DAA0001A4412F0FF0202D0521CD2B20171C378A4 +:10DAB00002F03F0103F0C0031943C170511C4170D3 +:10DAC00070472DE9F0410546C078164600F03F0446 +:10DAD0001019401C0F46FF2888BFFFDF2819324667 +:10DAE0003946001D07F04AFDA019401C6870BDE8CA +:10DAF000F081C178407801F03F01401A401E80B2A9 +:10DB0000704710B590F803C00B460CF03F01447805 +:10DB10000CF03F0CA4EB0C0CACF1010C1FFA8CF4D4 +:10DB2000944288BF14462BB10844011D2246184672 +:10DB300007F024FD204610BD4078704700B50278FC +:10DB400001F0030322F003021A430270012914BFFB +:10DB50000229002104D0032916BFFFDF012100BDE7 +:10DB6000417000BD00B5027801F0030322F003020A +:10DB70001A430270012914BF0229002104D003298D +:10DB800016BFFFDF012100BD417000BD007800F02D +:10DB900003007047417841B1C078192803D2C04AC8 +:10DBA000105C884201D1012070470020704730B5D9 +:10DBB00001240546C17019293CBFB948445C02D311 +:10DBC000FF2918BFFFDF6C7030BD70B515460E46DB +:10DBD00004461B2A88BFFFDF65702A463146E01CD9 +:10DBE000BDE8704007F0CABCB0F807007047B0F855 +:10DBF00009007047C172090A01737047B0F80B0041 +:10DC0000704730B4B0F80720B0F809C0B0F805305C +:10DC10000179941F40F67A45AC4298BFBCF5FA7F73 +:10DC20000ED269B1082998BF914209D293429FBF91 +:10DC3000B0F80B00B0F5486F012030BC98BF7047BA +:10DC4000002030BC7047001D07F04DBE021D084685 +:10DC5000114607F048BEB0F80900704700797047D8 +:10DC60000A68426049688160704742680A6080685B +:10DC700048607047098881817047808908807047B3 +:10DC80000A68C0F80E204968C0F812107047D0F832 +:10DC90000E200A60D0F81200486070470968C0F88A +:10DCA00016107047D0F81600086070470A68426086 +:10DCB00049688160704742680A60806848607047C0 +:10DCC0000968C1607047C068086070470079704794 +:10DCD0000A68426049688160704742680A608068EB +:10DCE000486070470171090A417170478171090AE2 +:10DCF000C17170470172090A417270478172090A45 +:10DD0000C172704780887047C0887047008970472B +:10DD10004089704701891B2924BF4189B1F5A47F3F +:10DD200007D381881B2921BFC088B0F5A47F0120BB +:10DD30007047002070470A684260496881607047F8 +:10DD400042680A60806848607047017911F0070FE7 +:10DD50001BBF407910F0070F0020012070470179A8 +:10DD600011F0070F1BBF407910F0070F00200120B2 +:10DD70007047017170470079704741717047407971 +:10DD800070478171090AC1717047C088704745A208 +:10DD900082B0D2E90012CDE900120179407901F098 +:10DDA000070269461DF80220012A07D800F0070083 +:10DDB000085C01289EBF012002B07047002002B01D +:10DDC0007047017170470079704741717047407921 +:10DDD000704730B50C460546FB2988BFFFDF6C70E5 +:10DDE00030BDC378024613F03F0008BF70470520DE +:10DDF000127903F03F0312F0010F36D0002914BF4F +:10DE00000B20704712F0020F32D0012914BF801D81 +:10DE1000704700BF12F0040F2DD0022914BF401C20 +:10DE2000704700BF12F0080F28D0032914BF801CD0 +:10DE3000704700BF12F0100F23D0042914BFC01C7C +:10DE4000704700BF12F0200F1ED005291ABF1230F4 +:10DE5000C0B2704712F0400F19D006291ABF401CFB +:10DE6000C0B27047072918D114E00029CAD114E0C4 +:10DE70000129CFD111E00229D4D10EE00329D9D153 +:10DE80000BE00429DED108E00529E3D105E00629ED +:10DE9000E8D102E0834288BF70470020704700004D +:10DEA000805C020000010102010202032DE9F04141 +:10DEB000FC4E0446736893F828000127002528B11A +:10DEC00093F8A001D8B993F84801C0B193F848017C +:10DED00098B383F8A071D3F84C113C2269B36570F4 +:10DEE000201D07F04BFB052020702771706890F80B +:10DEF000A011002918BF80F8485107D034E083F8FA +:10DF0000A05103F12A014FF48E72E7E71D212A3058 +:10DF100007F0B3FB70687F2180F84510FF2180F87F +:10DF2000381080F82B1080F83E10818E21F06001AF +:10DF30002031818680F8285016E0FFE793F8220010 +:10DF4000012814D0187801281BD093F8500101281B +:10DF50001CBF0020BDE8F081657018202070D3F848 +:10DF60005201606083F850510120BDE8F081657076 +:10DF700007202070586A606083F822500120BDE8B5 +:10DF8000F0816570142020702022991C201D07F05C +:10DF9000F5FA257271680D7081F85051C248828877 +:10DFA0008284D0F86421527B80F8262080F8227089 +:10DFB000D1F864010088F4F74FFEF4F7F6FAD3E7DE +:10DFC000B84840680178002914BF80884FF6FF7078 +:10DFD000704770B5B34C0546606890F874112046E0 +:10DFE0000629806803D0FFF73BFDB8B127E0FFF7B3 +:10DFF00037FD10BBA068FFF733FD00BB606890F8E9 +:10E00000A40110F00C0F1AD0A068C17811F03F0FD6 +:10E010001CBF007910F0100F11D00EE0616891F86C +:10E020007401082809D025B191F83E00FF2806D0D8 +:10E0300003E091F82B00FF2801D0012070BD0020E3 +:10E0400070BDF8B5974C07460E46606890F82810EA +:10E05000002906BF90F848110029F8BD00F13305EA +:10E0600020787F2808BFFFDF207828707F2020706D +:10E07000606890F89A1100F5D470085C012808BF18 +:10E08000012508D0022808BF022504D0042816BFA5 +:10E0900008280325FFDF606880F8365090F8971154 +:10E0A00080F8461090F87411072911D190F8A40156 +:10E0B000012808BF012508D0022808BF022504D086 +:10E0C000042816BF08280325FFDF606880F8375052 +:10E0D000606890F874014FF00005062804D1A0682C +:10E0E000FFF7BEFC00283CD0606890F87411082946 +:10E0F00004BF90F8A10102280ED04FF00301A068E0 +:10E10000FFF762FB40B141780A09616881F8382065 +:10E110000088C0F30B0048870095A068FFF7C2FA9B +:10E120006168BDF8005091F83420520962F3461539 +:10E13000ADF80050072818BFFFDF1CD0BDF8000065 +:10E1400000906068BDF8001081860421A068FFF788 +:10E150003BFB00287DD0B0F80100C004C00C79D092 +:10E16000B0E0A068C17811F03F0F1CBF007910F03B +:10E17000100FB9D1D0E791F87401062816D00728FE +:10E1800036D0082873D00A2818BFFFDFD6D145F053 +:10E190000A00ADF8000091F83E10FF2914BF0121DC +:10E1A000002161F38200ADF80000C7E7A068FFF727 +:10E1B00057FC58B1012808BF45F0010046D002289D +:10E1C00014BFFFDF45F0020040D0B7E7A068C17878 +:10E1D00011F03F0F1CBF007910F0020FAED00120EC +:10E1E000FFF7F7FE002808BF45F004002ED0A5E792 +:10E1F000A068FFF735FCB0B1012804BF45F001006D +:10E20000ADF800000FD0022898D145F00200ADF81B +:10E210000000A168CA7812F03F0F1CBF097911F005 +:10E22000020F21D118E0A068C17811F03F0F1CBF88 +:10E23000007910F0020F05D1606890F83E00FF28C9 +:10E240003FF47CAFBDF8000040F00400ADF80000E2 +:10E2500074E72BE02FE00AE0616891F83E10FF2997 +:10E2600008BF20F00400F1D040F00400EEE791F880 +:10E270003E00FF281CBF45F00400ADF8000091F8F7 +:10E28000A1010228BDF800000CBF40F0080020F0FA +:10E290000800ADF800000CBF40F0020020F00200C2 +:10E2A000D4E7000078010020F41000206068818E1F +:10E2B00021F0600105E06068818E21F0600101F1CC +:10E2C00040018186606890F8741106290DD190F89C +:10E2D000A40110F00C0F08D0A068C17811F03F0F16 +:10E2E0001CBF007910F0100F10D1A068C17811F098 +:10E2F0003F0F0BD0017911F0400F07D04FF006010E +:10E30000FFF762FA6168007881F84500606890F86C +:10E310007401062804D00020FFF75BFE18BB04E060 +:10E32000022F18BF012FF6D1F8BDA068C17811F0F7 +:10E330003F0F33D0017911F0010F2FD0616801F147 +:10E340002C0791F8783101F12B05FF2B0CD03A46C0 +:10E3500029461846FDF728FF002808BFFFDF287868 +:10E3600040F00200287019E0FFF7C5F92870A06896 +:10E37000FFF798F9072804D23946A068FFF79DF9FE +:10E380000CE0A068FFF78EF9072807D10021A068EC +:10E39000FFF71AFA016839608088B8800120FFF71A +:10E3A00018FE80BBA068C17811F03F0F2BD0017917 +:10E3B00011F0020F27D0616801F13F0591F8762135 +:10E3C0006F1E1AB1022E18BF032E08D0FFF76AF98C +:10E3D00007280AD22946A068FFF77DF912E0D1F894 +:10E3E0005A012860B1F85E010BE0A068FFF75AF906 +:10E3F000072807D10121A068FFF7E6F90168296025 +:10E400008088A8803E70606890F87401062808BF74 +:10E41000F8BD072818BF082802D00A2806D0F8BD82 +:10E42000A068FFF71DFB022808BFF8BD606800F177 +:10E430004705A068FFF75DFB626892F83230C3F1D0 +:10E44000FF01884228BF084605D9918E21F060015E +:10E4500001F140019186C2B203EB0501A068FFF70C +:10E4600050FB616891F83220104481F83200F8BD09 +:10E470002DE9F047FB4D06466C6894F8280000280B +:10E4800018BFBDE8F0871D212A34204607F0F5F8B3 +:10E4900001272770A868FFF705F920B3012827D0C6 +:10E4A00002282AD0062818BFFFDF2BD004F11D0157 +:10E4B000A868FFF740F92072686804F1020904F1C6 +:10E4C000010890F87801FF2821D04A464146FDF71F +:10E4D0006BFE002808BFFFDF98F8000040F0020044 +:10E4E00088F8000031E0608940F013006081DDE7CA +:10E4F000608940F015006081DEE7608940F010001F +:10E500006081D3E7608940F012006081CEE7A8689F +:10E51000FFF7F1F888F80000A868FFF7C3F80728AC +:10E5200004D24946A868FFF7C8F80EE0A868FFF7CC +:10E53000B9F8072809D10021A868FFF745F9016853 +:10E54000C9F800108088A9F80400287804F10908A7 +:10E550007F2808BFFFDF287888F800004FF07F0988 +:10E5600085F80090277300206073FF20A073A17AC4 +:10E5700011F0040F08BF20752DD0686804F115084C +:10E5800004F1140A90F8761119B1022E18BF032E67 +:10E5900009D0A868FFF786F807280BD24146A8687B +:10E5A000FFF799F815E0D0F85A11C8F80010B0F844 +:10E5B0005E010CE0A868FFF775F8072809D1012172 +:10E5C000A868FFF701F90168C8F800108088A8F86A +:10E5D00004008AF8006084F81B90686890F897112E +:10E5E000217780F82870BDE8F047062003F077BC5B +:10E5F0002DE9F0419B4C606890F82810FF2500271A +:10E60000A1B91D212A3007F038F860687F2180F811 +:10E61000451080F8385080F82B5080F83E50818E9D +:10E6200021F060012031818680F82870606800F553 +:10E63000D47290F89A11895C80F8A411002003F03C +:10E640005EF818B3F8F7DAFC6068874990F879014A +:10E650000E5C3046F8F74DFA606880F8976190F8E4 +:10E66000A41111F00C0F0CBF25200F20F8F74CF966 +:10E67000606890F8A4110120F8F7AFFA606890F88C +:10E680006811032918BF022910D103E0BDE8F04149 +:10E6900001F040B990F89A1100F5D470085C012897 +:10E6A00004D1012211460020F8F7BAFDF8F788FDE1 +:10E6B000606890F8A461012E07BF4FF001080321A4 +:10E6C0004FF000080521A068FDF74CFE616881F855 +:10E6D000760150B1B8F1000F18BF402623D000BF1B +:10E6E000F7F70FFF3046F8F74CFD6068D0F87C0173 +:10E6F000F8F790FC606890F87811FF291CBF00F2D1 +:10E700009110FDF768FD6068062180F8775180F868 +:10E71000785180F8867180F8857180F8A17180F851 +:10E720007411BDE8F08116F00C0F14BF5526502669 +:10E73000D6E770B54B4C0646606800F5BA752046C2 +:10E74000806841B1D0F80510C5F81D10B0F8090077 +:10E75000A5F8210003E005F11D01FEF7AEFFA0685A +:10E76000FEF7C9FF85F82400A0680021032E018070 +:10E7700002D0052E04D046E00321FEF771FF42E0EF +:10E780000521FEF76DFF6068D0F8640100F10E010D +:10E79000A068FEF7F4FF6068D0F8640100F1120190 +:10E7A000A068FEF7F0FFD4E90110D1F86421527D92 +:10E7B0008275D1F86421D28AC275120A0276D1F824 +:10E7C000642152884276120A8276D1F864219288B6 +:10E7D000C276120A0277D1F86421D2884277120AEF +:10E7E0008277D1F864110831FEF7EBFF6068D0F84A +:10E7F0006401017EA068FEF7CCFF606890F8AA1162 +:10E80000A068FEF7D0FF05F11D01A068FEF75CFFD0 +:10E8100095F82410A068FEF772FF606800F5AD75EA +:10E8200090F8596190F8751191B190F86811032929 +:10E8300006D190F86111002918BF90F87A0101D132 +:10E8400090F87701FDF7DDFD00281CBF0126054685 +:10E850002946A068FEF72AFF3146A068BDE870404F +:10E86000FEF740BF780100209C5C0200FD4949682A +:10E8700081F87301704770B5FA4D686890F87411AB +:10E8800002291FBF90F8741101290C2070BD00F1FE +:10E8900066014FF00004C0F84C1180F848414FF079 +:10E8A0001D0100F12A0006F0E8FE68687F2180F86B +:10E8B0004510FF2180F8381080F82B1080F83E10AA +:10E8C000818E21F060012031818680F8284004701B +:10E8D00080F8224080F85041012680F8A06190F82D +:10E8E000760130B1F8F757FCF7F71FFE686880F83B +:10E8F00076416868072180F8724180F8616180F88C +:10E90000684180F8794180F8734180F8A14180F82E +:10E910006011002070BDD34910B58860486800219F +:10E92000A0F8A51180F8A711012180F87411FFF754 +:10E93000A2FF002818BFFFDF10BD2DE9F041C94D2F +:10E940000446686890F87401012818BF022804D0B2 +:10E9500003281CBF0C20BDE8F081607A022823D078 +:10E96000F8F714F80220F8F74FFB686890F9730184 +:10E97000F8F7B1F8A868F8F74AFBBB48F8F72AFBA4 +:10E98000BA48F8F7AEF8686890F8591100F5AD701C +:10E99000F8F759F80F210720F8F771F8686890F830 +:10E9A0006101F0B1FDF7A0FC6868217A00F5D4722E +:10E9B00080F89A11217A895C80F8A4116168C0F806 +:10E9C0007C112168C0F88011627A6AB1012A23D0D3 +:10E9D0000524022A08BF80F8744175D0032A7FD02D +:10E9E00087E0FDF73CFCDFE7A14C90F860C1002117 +:10E9F00090F87921521CA4FB02635B08A3EB83030C +:10EA00001A4480F879212CFA02F212F0010F03D196 +:10EA1000491CC9B20329EBD3002680F8A16190F804 +:10EA20007111002904BF90F87501002848D0F6F74D +:10EA300023F9044668682146D0F86C01F6F735FEE4 +:10EA4000DFF83082074690FBF8F008FB1070414277 +:10EA50002046F5F712FE6968C1F86C0197FBF8F0E3 +:10EA6000D1F89C211044C1F89C01FDF775FB6A6840 +:10EA7000D2F89C11884223D8C2F89C61C2F86C413C +:10EA800092F8750100281CBF0120FDF787FC0121C9 +:10EA9000686890F87221002A1CBF90F87121002A42 +:10EAA0000ED090F8592100F5AD73012A04D15A799E +:10EAB00002F0C002402A09D000F5AD70F9F7F2F873 +:10EAC0006968042081F8740113E009E00124FDF76E +:10EAD00096FC6968224601F5AD71F9F7ACF8EFE7ED +:10EAE000002918BFFFDF012000F066FF686880F88A +:10EAF00074410020BDE8F08170B55A4C606890F810 +:10EB00007411042932D005291CBF0C2070BD90F867 +:10EB1000A1110026002900F2A51190F8A7114FEAD3 +:10EB2000511126D0002908BF012507D0012908BFAF +:10EB3000022503D0022914BF00250825D0F8800142 +:10EB400000281CBF002000F037FF6068D0F87C016F +:10EB5000F8F760FA606890F8681102293DD003293F +:10EB600004BF90F8900101283BD03FE0FFF740FD43 +:10EB700044E0002908BF012507D0012908BF02256C +:10EB800003D0022914BF00250825D0F880010028F1 +:10EB90001CBF002000F010FF6068D0F87C01F8F77F +:10EBA00039FA606890F86811022906D0032904BF79 +:10EBB00090F89001012804D008E090F89001022814 +:10EBC00004D12A4601210020F8F72AFB60680721BA +:10EBD00080F8A45180F885610EE090F89001022839 +:10EBE00004D12A4601210020F8F71AFB60680821A9 +:10EBF00080F8A45180F8856180F87411002070BD00 +:10EC00001849002210F0010F496802D0012281F852 +:10EC1000A82110F0080F03D01144082081F8A801A2 +:10EC2000002070470F49496881F87001704710B59E +:10EC30000C4C636893F85831022B14BF032B002847 +:10EC40000BD100291ABF0229012000201146FDF72F +:10EC500086FA08281CBF012010BD606890F8580192 +:10EC6000002809E078010020995C02009F5C020006 +:10EC7000ABAAAAAA40420F0016BF0228002001201A +:10EC8000BDE81040F8F798BFFE48406890F858017A +:10EC9000002816BF022800200120F8F78DBFF9498F +:10ECA000496881F858017047F649496881F872014E +:10ECB000704770B5F34C616891F85801002816BF91 +:10ECC00002280020012081F8590101F5AD71F8F703 +:10ECD0005DFF606890F85811022916BF03290121D1 +:10ECE000002180F8751190F8592100F5AD734FF0AF +:10ECF0000005012A04BF5B7913F0C00F0AD000F5AC +:10ED0000AD73012A04D15A7902F0C002402A01D021 +:10ED1000002200E0012280F87121002A04BF0029AE +:10ED200070BDC0F89C51F5F7A7FF6168C1F86C0190 +:10ED300091F8750100281CBF0020FDF72FFB00266D +:10ED4000606890F8721100291ABF90F871110029BB +:10ED500070BD90F8592100F5AD71012A04D14979AF +:10ED600001F0C001402906D02946BDE8704000F5F9 +:10ED7000AD70F8F797BFFDF742FB61683246BDE81A +:10ED8000704001F5AD71F8F756BF70B5BD4D0C463A +:10ED900000280CBF01230023696881F8613181F8E4 +:10EDA0006A014FF0080081F87A010CD1002C1ABFDB +:10EDB000022C012000201146FDF7D1F969680828CE +:10EDC00081F87A0101D0002070BD022C14BF032C01 +:10EDD0001220F8D170BD002818BF112070470328F9 +:10EDE000A84A526808BFC2F8641182F8680100207E +:10EDF000704710B5A34C606890F8681103291CBFD8 +:10EE0000002180F8841101D0002010BD0123D0F82A +:10EE100064111A460020FEF708FA6168D1F86421EF +:10EE2000526A904294BF0120002081F88401EBE7F0 +:10EE30009448416891F86801032804D0012818BF5C +:10EE4000022807D004E091F86A01012808BF704742 +:10EE50000020704791F86901012814BF03280120A0 +:10EE6000F6D1704770B5F8F780F9F8F75FF9F8F761 +:10EE700037F8F8F7B5F8834C0025606890F876010C +:10EE800030B1F8F788F9F7F750FB606880F87651F1 +:10EE900060680121A0F8A55180F8A75180F874118D +:10EEA00080F85051002070BD764810B5406800F5DC +:10EEB000C47006F0A8F8002010BD72480121406817 +:10EEC00090F86821032A03BF80F85211D0F864211A +:10EED0001288002218BF80F85221A0F8542180F82F +:10EEE000501170476749496881F8AA017047017855 +:10EEF000002311F0010F634949680AD04278032AC0 +:10EF000008BFC1F8643181F86821012281F8A82185 +:10EF10001346027812F0040F0CD082784FF0000CE8 +:10EF2000032A08BFC1F864C181F868210B44082294 +:10EF300083F8A821C27881F858210279002A16BFE7 +:10EF4000022A0123002381F8613181F86921427985 +:10EF500081F86021807981F870014FF000007047DE +:10EF60004848406800F5D27070472DE9F041454CA3 +:10EF700005460E46606890F87401032818BFFFDF4D +:10EF8000022D1EBF032DFFDFBDE8F0814FF000070B +:10EF90004FF00105AEB1606890F8371089B1818EED +:10EFA00021F0600101F14001818690F8282042B9EA +:10EFB00080F8285011F0080F14BF0720062002F037 +:10EFC0008EFF6068A0F8A57180F8A77180F8745171 +:10EFD000BDE8F08100F09EBC2DE9F047294C0646C3 +:10EFE000894660684FF00108072E90F8617138BFBC +:10EFF000032533D3082E4FF0000088BFBDE8F0870B +:10F00000FEF7E7FF002878D1A068C17811F03F0F24 +:10F0100012D0027912F0010F0ED061684FF0050591 +:10F0200091F87621002A18BFB9F1000F16D091F897 +:10F03000A411012909D011E011F03F0F1ABF007986 +:10F0400010F0100F002F58D151E04FF001024FF097 +:10F050000501FDF7CCF8616881F87601A1680878B0 +:10F060002944C0F3801030B1487900F0C000402836 +:10F0700008BF012000D00020616891F876110029B6 +:10F0800002E000007801002018BF002807D0FDF73B +:10F09000C9F80146606880F8771180F8858160685A +:10F0A00090F87711FF292BD080F878110846FDF7EA +:10F0B000C6F840EA0705606890F87721FF2A18BF74 +:10F0C000002D10D0072E0ED3A068C17811F03F0F8D +:10F0D00009D0017911F0020F05D00B21FDF734F9A9 +:10F0E000606880F886812846BDE8F08705E0FCF777 +:10F0F00072FE002808BFBDE8F0870120BDE8F08758 +:10F10000A36890F8612159191B78C3F3801C00F2A1 +:10F1100077136046FCF7C3FE0546CCE72DE9F041C6 +:10F12000FE4C84B0A068FEF79BFC0126002550B180 +:10F13000022501287ED002287DD0F7F7D1FE04B049 +:10F140000620BDE8F081F7F7CBFE606890F8680113 +:10F15000032800F0C480A068C17811F03F0F05D0EB +:10F16000027912F0100F18BF012600D10026002EE0 +:10F1700014BF0822012211F03F0F43D0007932EA78 +:10F1800000013FD110F0020F06D00120FEF721FF51 +:10F19000002808BF012000D000208DF800508DF815 +:10F1A00004508DF80850FF27D0B102AA694601A883 +:10F1B00000F051FC606890F859719DF8000000283B +:10F1C00018BF47F002070BD1A068FEF7A1FA8046EE +:10F1D0000121A068FEF7F8FA4146F7F73CFC90B130 +:10F1E00066B1012000F0B9FB002878D03946002034 +:10F1F000FEF727FF606880F890516CE039460020E8 +:10F2000000F06CFB6BE0606890F86901032818BFA0 +:10F21000022864D19DF80400002860D09DF8000009 +:10F2200000285CD17EB1012000F097FB002856D069 +:10F23000FE2101E00CE032E00020FEF702FF6068F2 +:10F2400080F8905147E0FE21002000F047FB46E0A7 +:10F25000F7F746FEA0681821C27812F03F0F3ED0A3 +:10F26000027991433BD10421FEF7AEFA616891F82F +:10F270006821032A01BF8078B5EB501F91F8840103 +:10F2800000282CD04FF0010000F067FB38B3FF21BD +:10F290000120FEF7D6FE606880F890611BE0F7F76A +:10F2A0001FFE606890F86801032818D0A068182134 +:10F2B000C27812F03F0F12D0007931EA00000ED16F +:10F2C000012000F04AFB50B1FF210220FEF7B9FEF9 +:10F2D000606880F8905104B00320BDE8F08104B06C +:10F2E0000620BDE8F081F0B58C4C074683B060681D +:10F2F0006D460078002818BFFFDF002661688E7019 +:10F30000D1F8640102888A8042884A8382888A838D +:10F31000C088C88381F8206047B10121A068FEF74A +:10F3200053FA0546A0680078C10907E06946A0685D +:10F33000FEF7C3F9A0680078C0F380116068012768 +:10F3400090F87521002A18BF002904D06A7902F0CC +:10F35000C002402A26D090F87221002A18BF002946 +:10F3600003D0697911F0C00F1CD000F10E0006F037 +:10F37000B1FA616891F87801FF2819D001F108020B +:10F38000C91DFCF711FF002808BFFFDF6068C179C5 +:10F3900041F00201C171D0F891114161B0F89511AD +:10F3A000018310E02968C0F80E10A9884182E0E7C7 +:10F3B000D1F86401427ECA71D0F81A208A60C08BED +:10F3C00088814E610E8360680770D0F8642190F8E0 +:10F3D000731182F85710D0F864010088F3F73CFCF1 +:10F3E000F3F7D4F803B0F0BD2DE9F0414B4C0546DE +:10F3F00001276068002690F86811012918BF0229CA +:10F4000002D0032918BFFFDF55B1A068FEF734FA18 +:10F4100018B9A068FEF787FA10B100F0C6FB2DE01E +:10F42000606890F874017F25801F062828BFBDE81A +:10F43000F081DFE800F003191930443E3748F7F750 +:10F44000CEFE002808BF2570F7F7B0FE606890F880 +:10F45000760130B1F7F79FFEF7F767F8606880F83C +:10F460007661F7F73DFD20E02C48F7F7B8FE00285D +:10F4700008BF2570F7F79AFE00F07DFB102880F09A +:10F480004481DFE800F036B9C2C6F7F712CFF6F7CD +:10F49000F7F7249F386C2148F7F7A1FE002808BF32 +:10F4A0002570F7F783FEF7F71BFDBDE8F041FFF786 +:10F4B0009FB81A48F7F793FE30B9257004E0174853 +:10F4C000F7F78DFE0028F8D0F7F770FE9DE00320D7 +:10F4D00002F015F9002874D000210320FFF729F964 +:10F4E000012211461046F7F79BFE61680C2081F857 +:10F4F0007401BDE8F081606800F5BA75042002F07F +:10F50000FEF800285DD00E202870012002F0E7FCF4 +:10F51000A06861680078C0F3401001E07801002025 +:10F5200081F8990100210520FFF703F9F749A06848 +:10F530004FF0200CD1F864210378527B23F0200394 +:10F540000CEA42121A430270D1F8640195F8253092 +:10F55000427B1A4042732820D1F864112DE0062026 +:10F5600002F0CDF8002850D0E84D0F2085F8740146 +:10F57000022002F0B4FC6068012190F8A421084642 +:10F58000F7F74EFEA06861680078C0F3401081F87C +:10F59000990101210520FFF7CCF8D5F864014773E4 +:10F5A000A068017821F020010170F8F720FA002806 +:10F5B00018BFFFDF2820D5F8641181F85600BDE898 +:10F5C000F08122E0052002F09AF8F0B10121032039 +:10F5D000FFF7AFF8F8F70BFA002818BFFFDF6068F5 +:10F5E000012190F8A4210846F7F71AFE61680D2062 +:10F5F00081F87401BDE8F0816068A0F8A56180F829 +:10F60000A76180F87471BDE8F081BDE8F04100F0B9 +:10F6100081B96168032081F87401BDE8F0410820D8 +:10F6200002F05DBC606890F8A711490908BF012588 +:10F6300007D0012908BF022503D0022914BF0025E5 +:10F640000825D0F8800100281CBF002000F0B4F984 +:10F650006068D0F87C01F7F7DDFC606890F868110D +:10F66000022908D0032904BF90F89001012806D090 +:10F670000AE010E049E090F89001022804D12A46FF +:10F6800001210020F7F7CCFD6068072180F8A45124 +:10F6900080F8856135E0606890F8A711490908BFD6 +:10F6A000012507D0012908BF022503D0022914BF74 +:10F6B00000250825D0F8800100281CBF002000F09C +:10F6C0007BF96068D0F87C01F7F7A4FC606890F8DB +:10F6D0006811022906D0032904BF90F8900101287F +:10F6E00004D008E090F89001022804D12A460121B4 +:10F6F0000020F7F795FD6068082180F8A45180F894 +:10F70000856180F87411BDE8F081FFDFBDE8F0810C +:10F7100070B57F4C606890F8743100210C2B38D0A4 +:10F7200001220D2B40D00E2B55D00F2B1CBFFFDF1D +:10F7300070BD042002F0D3FB606890F8A4110E2085 +:10F74000F7F7E2F8606890F8A40110F00C0F14BF0E +:10F75000282100219620F7F77BFCF7F731FD606840 +:10F76000052190F8A451A068FCF7FCFD616881F8C0 +:10F77000760148B115F00C0F0CBF50255525F6F752 +:10F78000C0FE2846F7F7FDFC61680B2081F8740184 +:10F7900070BDF7F715FD00219620F7F759FC616859 +:10F7A000092081F8740170BD90F8A411FF20F7F7CB +:10F7B000ABF8606890F8A40110F00C0F14BF28217A +:10F7C00000219620F7F744FCF7F7FAFC61680A205D +:10F7D00081F8740170BDA0F8A51180F8A71180F818 +:10F7E00074210020FFF77FFDBDE87040032002F088 +:10F7F00076BB70B5464C606890F874117F25891F00 +:10F80000062928BF70BDDFE801F017321D033D1146 +:10F810003F48F7F7E4FC002808BF2570F7F7C6FC5F +:10F82000F7F75EFBBDE87040FEF7E2BE3848F7F739 +:10F83000D6FC60BB25702AE03548F7F7D0FCD8B974 +:10F84000257019E090F8371089B1818E012221F0DE +:10F8500060014031818690F8283043B980F8282033 +:10F8600011F0080F14BF0720062002F038FB2848CB +:10F87000F7F7B5FC0028E3D0F7F798FCBDE8704037 +:10F8800000F048B82248F7F7AAFC0028D2D0F7F7D2 +:10F890008DFC6068002100F5C47005F065FBBDE8D3 +:10F8A000704000F037B870B5194C06460D46012976 +:10F8B00008D0606890F8A4213046BDE87040134637 +:10F8C00002F059BBF6F7D6FF61680346304691F85F +:10F8D000A4212946BDE8704002F04DBB10B5FEF7EB +:10F8E000B0FB0B48406890F82810002918BF10BDE5 +:10F8F000012280F8282090F8340010F0080F14BF7F +:10F9000007200620BDE8104002F0E9BAF4100020FC +:10F910007801002070B5F7F728FCF7F707FCF7F738 +:10F92000DFFAF7F75DFBFE4C0025606890F8760182 +:10F9300030B1F7F730FCF6F7F8FD606880F87651E3 +:10F940006068022180F87411A0F8A55180F8A751D1 +:10F95000BDE87040002002F0C2BA70B5F04D064616 +:10F960000421A868FDF730FF0446686890F8280075 +:10F97000A0B901F0A7FE217811F0800F14BF4FF459 +:10F9800096711E21B4F80120C2F30C0212FB01F1A2 +:10F990000A1AB2F5877F28BF814201D2002070BDCC +:10F9A00068682188A0F8A511A17880F8A7113046D1 +:10F9B000BDE8704001F0A3BE2DE9F041D84C0746E8 +:10F9C000606800F2A51690F8A701400908BF01255C +:10F9D00007D0012808BF022503D0022814BF002544 +:10F9E0000825F7F70BFB307800F03F063046F7F7B5 +:10F9F00080F8606880F8976190F8900102280CBF49 +:10FA00004020FF202946F6F77FFF27B12946012035 +:10FA1000F7F763F906E060682A46D0F88011012004 +:10FA2000F7F7A4F9F7F7CCFB0521A068FCF79AFCDF +:10FA30006168002881F8760108BFBDE8F08115F003 +:10FA40000C0F0CBF50245524F6F75BFD2046BDE893 +:10FA5000F041F7F796BB2DE9F74FB14C00259146E1 +:10FA600060688A4690F8750100280CBF4FF00108C5 +:10FA70004FF00008A0680178CE090121FDF7A4FE2F +:10FA800036B1407900F0C000402808BF012600D000 +:10FA90000026606890F87611002963D090F868110C +:10FAA0004FF0000B03291ED190F86111002918BFF7 +:10FAB00090F87A7117D0FF2F18BF082F22D0384640 +:10FAC000FCF730F9002818BF4FF00108002E49D08C +:10FAD000606890F88601D0B1FCF7AFFB054660681E +:10FAE00080F886B13EE0A168CA7812F03F0F19BFD6 +:10FAF000097911F0010F90F82B10FF2918BF90F829 +:10FB00007771D8D176B390F8850170B12AE0384684 +:10FB1000FCF741FB05460121A068FDF755FE0146B3 +:10FB20002846F8F757F805461CE0A068C17811F0A0 +:10FB30003F0F05D0017911F0010F18BF0B2101D142 +:10FB40004FF005014FF00002FCF751FB616881F8AE +:10FB5000760138B1FCF766FBFF2803D06168012508 +:10FB600081F877018AF800500098067089F80080C3 +:10FB700003B0BDE8F08F6A4810B5406890F83710C0 +:10FB800089B1818E012221F060014031818690F897 +:10FB9000283043B980F8282011F0080F14BF07203F +:10FBA000062002F09CF9022010BD2DE9F04F5C4DBB +:10FBB00083B00024686890F874017F27801F264670 +:10FBC0004FF00108062880F04082DFE800F00308CB +:10FBD0000893FEFD00F01EFC044600F037BA5048C2 +:10FBE000F7F7FDFA002808BF2F70F7F7DFFAA868CB +:10FBF000FDF758FD044607286AD1A868FDF730FFD5 +:10FC0000696891F89021824262D191F874010628C6 +:10FC100004D1A868FDF724FF002836D0686890F862 +:10FC20007411082904BF90F8A101022813D04FF0E5 +:10FC30000301A868FDF7C8FD002849D0696843782A +:10FC400091F83820B2EB131F42D10088498FC0F3DE +:10FC50000B0088423CD100212046FFF7BDF9B0B32C +:10FC60008DF800608DF804608DF80860A868FF24A6 +:10FC7000C17811F03F0F1CBF007910F0020F1CD0AB +:10FC80000120FEF7A6F950B117E0A868C17811F07D +:10FC90003F0F1CBF007910F0100FBFD1DBE702AAA5 +:10FCA000694601A8FFF7D7FE686890F859419DF8AA +:10FCB0000000002818BF44F0020423469DF80820E5 +:10FCC0009DF804109DF8000000F012FA02E0FFE732 +:10FCD000FFF751FF0446686890F87601002800F0AD +:10FCE000B581F7F758FAF6F720FC686880F8766176 +:10FCF00000F0ACB9A868FDF7D5FC8146A968686832 +:10FD0000CA7890F891319A4224D10A7990F89231C8 +:10FD10009A421FD14A7990F893319A421AD101E060 +:10FD2000780100208A7990F894319A4212D1CA79E8 +:10FD300090F895319A420DD10A7A90F896319A420C +:10FD400008D1097890F89801C1F38011814208BF69 +:10FD5000012400D00024F7F7C3F8FB48F7F73FFA77 +:10FD6000002808BF2F70F7F721FAB9F1040F76D1F8 +:10FD7000002C74D0686890F8481100296FD190F871 +:10FD8000281021B190F8341011F0100F67D0D0F87E +:10FD90004C411D21204605F070FC84F80080686805 +:10FDA00004F1020A04F1010990F87801FF2810D04B +:10FDB00052464946FCF7F8F9002808BFFFDF99F8DA +:10FDC000000040F0020001E04CE0FFE089F8000094 +:10FDD0001DE0A868FDF78FFC89F80000A868FDF712 +:10FDE00061FC072804D25146A868FDF766FC0EE0C6 +:10FDF000A868FDF757FC072809D10021A868FDF77E +:10FE0000E3FC0168CAF800108088AAF8040004F135 +:10FE10001D01A868FDF78FFC2072287804F10909FC +:10FE20007F2808BFFFDF287889F800002F706868F6 +:10FE3000618990F8A12162F3000141F01A0161810A +:10FE400084F80C806673FF21A1732175E77690F822 +:10FE50009711217780F84881072002F040F80624A6 +:10FE600000F0F4B84FF00208B748F7F7B8F90028E7 +:10FE700008BF2F70F7F79AF9A868FDF713FC04463E +:10FE8000A868FDF7EDFD082C08BF00287ED1A86802 +:10FE90004FF00301C27812F03F0F77D0007931EABA +:10FEA000000073D1686800F5BA7790F86101002806 +:10FEB00014BFBE79FE784FF00009B87878B1FCF72E +:10FEC000B1F90446FF280AD00146A868401DFCF796 +:10FED00082F9B4420CBF4FF001094FF00009002134 +:10FEE000A868FDF771FC062207F11D0105F01AFB59 +:10FEF00040B9A868FDF7FFFB97F82410884208BFB7 +:10FF0000012000D0002059EA00095DD0686800F5A2 +:10FF1000AD7490F859A1787838B13046FCF771FA91 +:10FF200000281CBF04464FF0010A0027A86801788A +:10FF30004FEAD11B0121FDF747FCBBF1000F07D0B1 +:10FF4000407900F0C000402808BF4FF0010B01D0FD +:10FF50004FF0000B0121A868FDF736FC0622214670 +:10FF600005F0E0FA30B9A868FDF7D2FB504508BFAC +:10FF7000012401D04FF000043BEA040018BFFF2E1B +:10FF80000FD03046FCF707F9060000E01CE008D06F +:10FF90000121A868FDF718FC01463046F7F71AFE64 +:10FFA000074644EA070019EA000F0DD068680121EE +:10FFB00000F5C47004F0D8FF4FF001084046FFF789 +:10FFC00092F9052001F08BFF44463FE002245E4891 +:10FFD000F7F705F9002808BF2F70F7F7E7F8A868CA +:10FFE000FDF760FB0646A868FDF73AFD072E08BF3F +:10FFF00000282BD1A8684FF00101C27812F03F0F02 +:020000040002F8 +:1000000024D00279914321D1696801F5BA760021A3 +:10001000FDF7DAFB062206F11D0105F083FAA8B907 +:10002000A868FDF768FB96F8241088420ED168682E +:10003000012100F5C47004F097FFFF21022000F0B9 +:1000400009F8002818BF032400E0FFDF03B02046B2 +:10005000BDE8F08F2DE9F0413B4C02460025606879 +:1000600090F8A1310BB3A0684FF000064FF00107E4 +:10007000C37813F03F0F1CBF007910F0100F1BD096 +:100080000020FDF7DEFF606890F83400C0F34110F7 +:1000900002281BD00220FFF760FC88B160680125B0 +:1000A00080F89061F6F71CFF1FE0002A14BF0223BE +:1000B000012380F8A131D6E71046FDF7C2FF05E025 +:1000C0006068818E21F0600140318186606890F81F +:1000D000281051B980F8287090F8340010F0080FFB +:1000E00014BF0720062001F0FAFE2846BDE8F08183 +:1000F0002DE9F047144C05461F4690460E46A06871 +:10010000FDF7AEFC002800F0D180012805D00228C0 +:1001100000F00E81BDE8F0472DE5A0680921C27806 +:1001200012F03F0F00F042810279914340F03E818E +:10013000616891F86811032908D012F0020F08BF16 +:10014000FF211BD075B118E0780100200021FDF7D8 +:100150003BFB61680622D1F864111A3105F0E2F91F +:1001600050BB1EE0FDF7D4FA05460121A068FDF75B +:100170002BFB2946F6F76FFC18B13946012000F039 +:1001800039B9606890F86901032818BF022840F067 +:100190000D81002E1CBFFE21012040F02B8100F0BC +:1001A00005B9A068FDF7A7FA6168D1F86411497E26 +:1001B000884208BF012600D00026A068C17811F04F +:1001C0003F0F05D0017911F0020F01D05DB338E087 +:1001D000616891F86A21012A01D0A6B119E0C6B977 +:1001E0000021FDF7F1FA61680268D1F86411C1F8E5 +:1001F0001A208088C883A068FDF77DFA6168D1F86D +:100200006411487605E091F8770191F87A118842F7 +:100210004BD1606800F5C47004F0EAFE002844D0B9 +:100220000F20BDE8F087B8F1000F0CD0FDF770FA91 +:1002300005460121A068FDF7C7FA2946F6F70BFC31 +:1002400008B1012200E00022616891F86A010128EA +:1002500007D040B92EB991F8773191F87A118B42D5 +:1002600001D1012100E000210A421ED0012808BF6F +:10027000002E13D14FF00001A068FDF7A5FA6168C8 +:100280000268D1F86411C1F81A208088C883A06878 +:10029000FDF731FA6168D1F864114876606800F5BD +:1002A000C47004F0A5FE0028BAD17FE06068A846BB +:1002B0004FF0020990F8680103282AD0A068C1789D +:1002C00011F03F0F1BBF007910F0020F002001203A +:1002D0004FF0FF05A8B14FF00100FDF77AFE0028AE +:1002E00004BF3D46B8F1000F0BD1A068FDF710FA2E +:1002F00007460121A068FDF767FA3946F6F7ABFB20 +:1003000050B129460020FFF7A5FE002818BF4FF086 +:1003100003094846BDE8F087606890F86901032842 +:1003200018BF0228F5D1002E18BFFE25E9D1F0E74D +:10033000626892F86831032B38D0A0684FF0090C3E +:10034000C17811F03F0F31D001793CEA010C2DD179 +:10035000022B01F0020105D0002908BFFF2147D080 +:10036000CDB344E009B135B113E002F5C47004F037 +:100370003FFEA0B91AE0B8F1000F1AD0FDF7C8F996 +:1003800005460121A068FDF71FFA2946F6F763FB31 +:1003900078B1606800F5C47004F02AFE30B13946C7 +:1003A0000220FDF74EFE0D20BDE8F0870220BDE8DB +:1003B000F087606890F86901032818BF0228F5D11A +:1003C000002EF3D04FF0FE014FF00200FFF786FA47 +:1003D0000220BDE8F087FFE7FDF79AF90546012105 +:1003E000A068FDF7F1F92946F6F735FB28B1394643 +:1003F0005FF00200FFF772FAD8E7606890F86901D1 +:10040000032818BF0228D1D1002E1CBFFE210220D4 +:10041000F0D1CBE72DE9F84F0027D048F6F7DFFE03 +:10042000CE4C002804BF7F202070F6F7BFFEA068E6 +:10043000FDF738F980460121FEF7CEFD61684FF0E7 +:10044000000B91F8A421012A13D0042A1CBF082A0A +:10045000FFDF00F07781606890F8760130B1F6F741 +:100460009AFEF6F762F8606880F876B13846BDE823 +:10047000F88F0125BA4EB8F1080F19D2DFE808F05D +:1004800024860418181811FD0546F6F729FD002DDD +:100490007AD0606890F86801012818BF022858D007 +:1004A00072E028B191F86801022805D0012850D0E7 +:1004B000F6F716FD0627CEE7FF20FDF7D9FF6068A7 +:1004C0000C2780F8A1B1C6E70027002800F02081A2 +:1004D00091F86801022834D001283AD00328BAD113 +:1004E000A068D1F86421C37892F81AC0634521D17D +:1004F000037992F81BC063451CD1437992F81CC064 +:10050000634517D1837992F81DC0634512D1C37931 +:1005100092F81EC063450DD1037A92F81FC063455F +:1005200008D1037892F819C0C3F38013634508BF5C +:10053000012300D0002391F86A1101290DD0D3B115 +:10054000E4E0FF20FDF794FF60680C2780F8A151DC +:1005500081E7FF20FDF78CFF16E0002B71D102F13F +:100560001A01FDF7AAF8A068FDF7C5F86168D1F88F +:1005700064114876CAE096F87A0108287CD096F88B +:10058000771181425DD0C3E0062764E7054691F804 +:10059000750100280CBF4FF001094FF0000900273A +:1005A000A06810F8092BD20907D0407900F0C000EC +:1005B000402808BF4FF0010A01D04FF0000A91F81F +:1005C0006801032806D191F86101002818BF91F84D +:1005D0007A0101D191F877010090FBF7DCFD5FEA29 +:1005E00000082AD00098FBF79DFB002818BF4FF0A9 +:1005F0000109BAF1000F20D0A06800F109014046BE +:10060000F7F7E8FA0700606890F8598118BF48F0DA +:100610000208606890F86811032913D0F6F760FCAF +:10062000002DB1D0F6F727FA00280CBF002F404666 +:1006300072D000BFFDF71CFFA6E7606890F85981F3 +:10064000E7E763E0A168D0F86401CA78837E9A4244 +:100650001FD10A79C37E9A421BD14A79037F9A42FD +:1006600017D18A79437F9A4213D1CA79837F9A42FC +:100670000FD10A7AC37F01E04AE05BE09A4208D1D9 +:100680000978407EC1F38011814208BF4FF0010814 +:1006900001D04FF0000896F87701082806D096F8A8 +:1006A0007A11884208BF4FF0010A01D04FF0000ACA +:1006B0002FB9B9F1000F04D0F6F7DDF908B1012028 +:1006C00000E000204DB196F86A11012903D021B94C +:1006D00058EA0A0101D0012100E00021084217D0A8 +:1006E000606890F86A11012908BFB8F1000F0DD1B8 +:1006F000D0F8640100F11A01A068FCF7DEFFA068E1 +:10070000FCF7F9FF6168D1F8641148760E27A2E67C +:10071000F6F7E6FB38E7FFE7606890F86901032821 +:1007200018BF02287FF430AFBAF1000F18BFFE20C7 +:1007300080D129E791F87011002918BF00283FF4F3 +:10074000B7AE06E0B8F1070F7FF4B2AE00283FF471 +:10075000AFAEFEF7E3FC07467DE60000780100201F +:10076000F4100020D0F8E81049B1D0E93B231A4436 +:100770008B691A448A61D0E93912D16003E0F74AE3 +:10078000D0F8E4101162D0E9391009B1086170475E +:100790000028FCD00021816170472DE9FF4F0646FB +:1007A0000C46488883B040F2E24148430190E08A19 +:1007B000002500FB01FA94F8640090460D2822D031 +:1007C0000C2820D024281ED094F8650024281AD0A4 +:1007D00000208346069818B10121204603F000F955 +:1007E00094F8541094F85500009094F8D8200F46CF +:1007F0004FF47A794AB1012A61D0022A44D0032AFF +:100800005DD0FFDFB5E00120E3E7B8F1000F00D1D4 +:10081000FFDFD24814F8541F243090F83800FCF75A +:1008200004FF01902078F7F75EF84D4600F2E730BC +:10083000B0FBF5F1DFF82493D9F80C0001EB0008C8 +:100840002078F7F750F8014614F85409022816D01A +:10085000012816D040F6340008444AF2EF0108445B +:10086000B0FBF5F10198D9F81C20411A514402EB74 +:1008700008000D18012084F8D8002D1D78E02846C6 +:10088000EAE74FF4C860E7E7DFF8D092A8F101008B +:10089000D9F80810014300D1FFDFB148B8F1000FCB +:1008A000016801EB0A0506D0D9F8080000F22330F0 +:1008B000A84200D9FFDF032084F8D80058E094F85C +:1008C0006420019D242A05D094F86530242B01D0A2 +:1008D000252A3AD1B4F85820B4F8F830D21A521C6C +:1008E00012B2002A31DB94F8FA2072B3174694F85A +:1008F000FB2002B110460090022916D0012916D023 +:1009000040F6340049F608528118022F12D0012F08 +:1009100012D040F634001044814210D9081A00F574 +:10092000FA70B0FBF9F005440FE04846EAE74FF4EF +:10093000C860E7E74846EEE74FF4C860EBE7401AC7 +:1009400000F5FA70B0FBF9F02D1AB8F1000F0FD0D6 +:10095000DFF80882D8F8080018B9B8F8020000B12A +:10096000FFDFD8F8080000F22330A84200D9FFDFEB +:1009700005B9FFDF2946D4F8DC00F3F77EFEC4F8A2 +:10098000DC00B060002030704FF0010886F8048071 +:10099000204603F080F8ABF10101084202D186F84D +:1009A000058005E094F8D80001282FD0032070714D +:1009B000606A3946009A01F026FBF060069830EA3A +:1009C0000B0020D029463046FCF752FB87B2204668 +:1009D00003F061F8B8420FD8074686F8058005FB9A +:1009E00007F1D4F8DC00F3F748FEB0602946304642 +:1009F000FCF73EFB384487B23946204602F0F0FF50 +:100A0000B068C4F8DC0007B0BDE8F08F0220CEE784 +:100A10002DE9F04106460C46012001F0D6FAC5B298 +:100A20000B2001F0D2FAC0B2854200D0FFDF0025D2 +:100A3000082C7DD2DFE804F00461696965C98E96EF +:100A4000304601F0D6FA0621F1F7D4FF040000D1B8 +:100A5000FFDF304601F0CDFA2188884200D0FFDF69 +:100A600094F8D80000B9FFDF204602F060FE3B4E4C +:100A700021460020B5607580F561FCF729FC00F186 +:100A80009807606AB84217D994F85500F6F712FF34 +:100A9000014694F854004FF47A72022828D00128B5 +:100AA00028D040F6340008444AF247310844B0FBED +:100AB000F2F1606A0844C51B214600203561FCF74D +:100AC00007FC618840F2E24251439830081AA0F2D4 +:100AD0002330706194F8552094F85410606A01F046 +:100AE00092FAA0F29310B061BDE8F041F4F7AABD0C +:100AF0001046D8E74FF4C860D5E7BDE8F04102F0F2 +:100B000080BEBDE8F041F6F7A7BB6FF0040001F02E +:100B10005CFAC4B2192001F058FAC0B2844200D085 +:100B2000FFDF304601F065FA0621F1F763FF00E0D0 +:100B30004BE0040000D1FFDF304601F05AFA218873 +:100B4000884200D0FFDF2046BDE8F04101220021AD +:100B500001F076BAF6F720FAD3E70000A0120020E1 +:100B600088010020304601F044FA0621F1F742FFE7 +:100B7000040000D1FFDF304601F03BFA21888842B3 +:100B800000D0FFDF94F8D800042800D0FFDF84F8FD +:100B9000D85094F8E2504FF6FF76202D00D3FFDFB7 +:100BA000FB4820F8156094F8E200F4F746F800B925 +:100BB000FFDF202084F8E2002046FFF7D3FDF54850 +:100BC0000078BDE8F041E2F7A7B9FFDFBDE8F081AA +:100BD00070B5EF4C0025483C84F82C50E07868B1A3 +:100BE000E570FEF76AF92078042803D0A06AFFF7C1 +:100BF000B9FDA562E7480078E2F78EF9BDE87040DC +:100C000001F02FBA70B5E24C0146483C206AF4F777 +:100C10004CFD6568A27890FBF5F172B140F271224B +:100C2000B5FBF2F292B2E36B01FB02F6B34202D9DA +:100C300001FB123200E00022E2634D43002800DA9B +:100C4000FFDF2946206AF3F718FD206270BD2DE909 +:100C5000F05FFEF785F98246CD486C3800F1240834 +:100C600081684646D8F81C00F3F707FD0146306A54 +:100C7000F4F71BFD4FF00009074686F839903C4613 +:100C80004FF423754E461CE00AEB06000079F6F798 +:100C900011FE4AF2B12101444FF47A70B1FBF0F138 +:100CA00008EB86024046926811448C4207D3641ACE +:100CB00090F83910A4F52374491C88F83910761C73 +:100CC000F6B298F83A00B042DED8002C0FDD98F862 +:100CD0003910404608EB81018968A14207D241687A +:100CE000C91BA94200D90D466C4288F8399098F882 +:100CF0003960C3460AEB060898F80400F6F7DAFDF7 +:100D000001464AF2B12001444FF47A7AB1FBFAF27B +:100D100098F80410082909D0042909D000201318D4 +:100D200004290AD0082908D0252007E0082000E07F +:100D3000022000EB40002830F1E70F20401D4FF467 +:100D4000A872082913D0042914D0022915D04FF015 +:100D5000080C282210FB0C20184462190BEB8603A8 +:100D600002449868D84682420BD8791925E04FF0A2 +:100D7000400CEFE74FF0100CECE74FF0040C18229A +:100D8000E8E798F8392098F83A604046B24210D225 +:100D9000521C88F839203C1B986862198418084650 +:100DA000F6F788FD4AF2B1210144B1FBFAF00119CE +:100DB00003E080F83990D8F80410D8F82000BDE896 +:100DC000F05FF3F75ABC2DE9FE4F14460546FEF7D7 +:100DD000C7F8DFF8BCB10290ABF1480B58469BF85E +:100DE00039604FF0000A0BEB86018968CBF84010A0 +:100DF000ECB3044600780027042827D0052840D00B +:100E0000FFDFA0463946A069F3F737FC0746F3F742 +:100E100033FF81463946D8F80440F4F746FC401EBB +:100E200090FBF4F0C14361433846F3F726FC0146DA +:100E3000C8F820004846F4F738FC002800DDFFDF42 +:100E4000012088F8140088F813008FE0D4F8189077 +:100E5000D4F8048001F06FF9070010D0387800B999 +:100E6000FFDF796978684A460844414600E00EE0B1 +:100E700001F049F907464045C3D9FFDFC1E75746AE +:100E8000BFE7A06A01F0FAF840F6B837B9E7016A9F +:100E90000BEB46000191C08D08B35C46DBF81800EF +:100EA000FFF7B0FE6168206AF3F7E7FB074684F8B6 +:100EB00039A0019CD8462046DBF81810F4F7F5FB62 +:100EC000814639462046F4F7F0FBD8F80420B9FBF8 +:100ED000F2F3B0FBF2F0834243D0012142E0F3F79A +:100EE000CBFEFFF78FFEFFF7B2FE9BF83910DBF861 +:100EF00004900BEB81010746896800913946DBF8C5 +:100F00002000F4F7D2FB00248046484504DB98FB20 +:100F1000F9F404FB09F41BE0002059469BF8392042 +:100F200008E000BF01EB800304F523749B68401CBC +:100F30001C44C0B28242F5D852B10120F6F7BAFC87 +:100F40004AF2B12101444FF47A70B1FBF0F004444D +:100F50000099A8EB04000C1A00D5FFDFCBF8404045 +:100F6000A7E7002188F8141088F813A09BF8020066 +:100F70005C46B8B13946206AF4F797FB0146E26B4C +:100F800040F2712042438A4206D2C4F840A009E0F0 +:100F90000C13002084010020206C511A884200D3D9 +:100FA00008462064AF6085F800A001202871029FE8 +:100FB00094F839003F1DC05DF6F77CFC4AF23B51C6 +:100FC00001444FF47A70B1FBF0F0216CFB3008441F +:100FD000E8602078042808D194F8390004EB400038 +:100FE000C08D0A2801D2032000E00220687104EBC2 +:100FF0004600C08DC0B128466168FCF739F882B25E +:101000000020761C0CE000BF04EB4003B042D98DF9 +:10101000114489B2D98501D3491CD985401CC0B27D +:1010200094F83A108142EFD2A868A061E06194F888 +:10103000390004EB4000C18D491CC18594F839008A +:10104000C05D082803D0042803D000210BE008214C +:1010500000E0022101EB410128314FF4A872082879 +:1010600004D0042802D0022807D028220A440428E9 +:1010700005D0082803D0252102E01822F6E70F2129 +:10108000491D08280CD004280CD002280CD00820B8 +:1010900011FB0020216C884208D20120BDE8FE8FA0 +:1010A0004020F5E71020F3E70420F1E70020F5E702 +:1010B00070B5FB4C061D14F8392F905DF6F7FAFB5E +:1010C0004FF47A7100F2E730B0FBF1F0D4F807107A +:1010D00045182078805DF6F7DBFB2178895D0829CB +:1010E00003D0042903D000220BE0082200E00222F2 +:1010F00002EB420228324FF4A873082904D00429D5 +:1011000002D0022907D028231344042905D0082936 +:1011100003D0252202E01823F6E70F22521D0829EA +:101120000AD004290AD002290AD0082112FB013171 +:10113000081A281A293070BD4021F7E71021F5E779 +:101140000421F3E7FEB504460F46012000F03DFF01 +:10115000C5B20B2000F039FFC0B2854200D0FFDFDE +:1011600001260025CE48082F50D2DFE807F00430D2 +:101170004747434F4F4C0446467406744078002856 +:1011800019D1FDF7EDFE009594F839108DF808108F +:101190004188C90410D0606C019003208DF80900CB +:1011A000BF4824388560C56125746846FDF7C5FBD6 +:1011B000002800D0FFDFFEBDFFF77AFF0190207D01 +:1011C00010B18DF80950EBE78DF80960E8E70446A7 +:1011D000407840B1207C08B9FDF744FE6574BDE855 +:1011E000FE40F3F753BCA674FDF786FC0028E2D05E +:1011F000FFDFFEBDBDE8FE40F6F72EB82046BDE895 +:10120000FE4000F0A1BFBDE8FE40E1E4FFDFFEBD0F +:10121000A34950B101228A704A6840F27123B2FB9F +:10122000F3F202EB0010C86370470020887070472B +:101230002DE9F05F894640F27121994E484300251F +:101240000446706090462F46D0074AF2B12A4FF408 +:101250007A7B0FD0B9F800004843B0600120F6F760 +:1012600029FB00EB0A01B1FBFBF0241AB76801254A +:10127000A4F523745FEA087016D539F8151040F20A +:101280007120414306EB85080820C8F80810F6F7DE +:1012900011FB00EB0A01B1FBFBF0241AD8F808009F +:1012A000A4F5237407446D1CA7421AD9002D18D049 +:1012B000391BB1FBF5F0B268101AB1FBF5F205FB72 +:1012C0001212801AB060012009E000BFB1FBF5F3F3 +:1012D00006EB80029468E31A401CC0B29360A842F7 +:1012E000F4D3BDE8F09F2DE9F0416D4C0026207845 +:1012F000042804D02078052801D00C2066E40120C1 +:101300006070607C002538B1EFF3108010F0010FA1 +:1013100072B610D001270FE0FDF722FE074694F8C1 +:101320002400F4F70EF87888C00411D000210320BF +:10133000FDF71BFE0CE00027607C38B1A07C28B1D3 +:10134000FDF790FD6574A574F3F7A0FB07B962B6CD +:1013500094F82400F4F743FA94F82C0030B184F8A0 +:101360002C502078052800D0FFDF0C26657000F097 +:1013700078FE30462AE44A4810B5007808B1FFF7F5 +:10138000B2FF00F011FF464900202439086210BD69 +:1013900010B5444C58B1012807D0FFDFA06841F6D2 +:1013A0006A01884200D3FFDF10BD40F6C410A06080 +:1013B000F4E73C4908B508703949002008704870C6 +:1013C00081F82C00C87008744874887420228862E0 +:1013D00081F82420243948704FF6FF7211F16C0116 +:1013E00021F81020401CC0B22028F9D30020FFF7BC +:1013F000CFFFFFF7C0FF1020ADF8000001226946C3 +:101400000420FFF715FF08BD7FB5254C05460E46A5 +:10141000207810B10C2004B070BD95F8552095F8D7 +:101420005410686A00F002FFC5F8EC00A56295F858 +:10143000D80000B1FFDF1A4900202439C861052116 +:101440002170607084F82C00014604E004EB410236 +:10145000491CD085C9B294F83A208A42F6D284F861 +:1014600039003046FFF7D4FE0F48F3F78AFB84F8C3 +:101470002400202800D1FFDFF3F7FEFBA06194F8E1 +:10148000241001226846FFF79EFC00B9FFDF94F8A4 +:1014900024006946F3F73AFE00B9FFDF0020BAE7FF +:1014A000C41200208401002045110200F84810B544 +:1014B000007808B1002010BD0620F1F735FA80F061 +:1014C000010010BDF8B5F24D0446287800B1FFDFE9 +:1014D0000020009023780246DE0701466B4605D0C7 +:1014E0006088A188ADF800100122114626787607A1 +:1014F00006D5E088248923F8114042F00802491CEF +:10150000491E85F83A101946FFF792FE0020F8BDF3 +:101510001FB511B1112004B010BDDD4C217809B107 +:101520000C20F8E70022627004212170114604E0CB +:1015300004EB4103491CDA85C9B294F83A308B4276 +:10154000F6D284F83920FFF763FED248F3F719FB8F +:1015500084F82400202800D1FFDF00F0ECFD10B15A +:10156000F3F78AFB05E0F3F787FB40F6B831F3F7B2 +:1015700084F8A06194F8241001226846FFF723FC48 +:1015800000B9FFDF94F824006946F3F7BFFD00B906 +:10159000FFDF0020BFE770B5BD4CA16A0160FFF717 +:1015A000A2FE050002D1A06AFFF7DCF80020A062CD +:1015B000284670BD7FB5B64C2178052901D00C2096 +:1015C00029E7B3492439C860A06A00B9FFDFA06ADF +:1015D00090F8D80000B1FFDFA06A90F8E200202860 +:1015E00000D0FFDFAC48F3F7CCFAA16A054620280B +:1015F00081F8E2000E8800D3FFDFA548483020F8CC +:101600001560A06A90F8E200202800D1FFDF0023D7 +:1016100001226846A16AFFF7C0F8A06A694690F8FF +:10162000E200F3F773FD00B9FFDF0020A062F2E6ED +:10163000974924394870704710B540F2E24300FBE7 +:1016400003F4002000F0F2FD844201D9201A10BDFD +:10165000002010BD70B50D46064601460020FBF780 +:1016600037FE044696F85500F6F724F9014696F839 +:1016700054004FF47A72022815D0012815D040F694 +:10168000340008444AF247310844B0FBF2F1708854 +:1016900040F271225043C1EB4000A0F22330A5423A +:1016A00006D2214605E01046EBE74FF4C860E8E7B4 +:1016B0002946814204D2A54201D2204600E02846B4 +:1016C000706270BD70B5F5F7D5F80446F6F7E0F82E +:1016D00001466F48243882684068101A0E18204668 +:1016E00000F06AFC05462046F6F7E4F8281A4FF4A5 +:1016F0007A7100F2E730B0FBF1F0304470BD70B5A4 +:101700000546FDF72DFC6249007824398C68983431 +:10171000072D30D2DFE805F0043434252C343400B2 +:1017200014214FF4A873042810D00822082809D0E7 +:101730002A2102280FD011FB024000222823D118B1 +:10174000441819E0402211FB0240F8E7102211FB77 +:1017500002402E22F3E7042211FB0240002218234C +:10176000EDE7282100F040FC044404F5317403E067 +:1017700004F5B07400E0FFDF4548006CA04201D9D9 +:10178000012070BD002070BD70B5414C243C6078D4 +:1017900070B1D4E904512846A268FBF794FC20619B +:1017A000A84205D0A169401B0844A061F3F74AFF95 +:1017B0002169A068884201D8207808B1002070BD56 +:1017C000012070BD2DE9F04F054685B016460F4645 +:1017D0001C461846F6F75CF805EB4701471820460B +:1017E00000F0EAFB4AF2C5714FF47A7908444D469D +:1017F000B0FBF5F0384400F16008254824388068D3 +:10180000304404902046F6F743F8A8EB0007204642 +:1018100000F0D2FB06462046F6F74CF8301AB0FB33 +:10182000F5F03A1A182128254FF4C8764FF4BF77FF +:101830004FF0020B082C34D0042C2FD00020022CA7 +:1018400032D0082310F1280003EB830C0CEB831338 +:10185000184402444FF0000A082C2DD0042C26D046 +:101860000020022C2DD0082100F5B07001EB0111F1 +:101870002944884232D2082C2AD0042C25D00020BA +:10188000022C28D00821283001EB011134E000009F +:10189000C412002045110200110A0200384610232C +:1018A000D2E730464023CFE704231830CCE73D464B +:1018B00040F2EE301021D9E735464FF43560402133 +:1018C000D4E70D460421B430D0E738461021DBE7D9 +:1018D00030464021D8E704211830D5E7082C4FD0F6 +:1018E000042C4AD00020022C4DD0082110F12800F1 +:1018F000C1EBC10303EB4111084415182821204610 +:1019000000F072FB05EB4000082C42D0042C3DD0C7 +:101910000026022C3FD0082116F1280601EB811188 +:1019200006EB810146180120FC4D8DF804008DF86E +:1019300000A08DF805B0E86906F227260499F2F7B1 +:101940009CFECDE902062046F5F7B4FF4AF23B5172 +:101950000144B1FBF9F0301AFB3828640298C5F84D +:101960004480E86195F824006946F3F7CFFB00282E +:1019700000D1FFDF05B0BDE8F08F38461021B7E792 +:1019800030464021B4E704211830B1E73E4610212B +:10199000C4E74021C2E704211836BFE72DE9FE4F16 +:1019A00004461D46174688464FF0010A1846F5F7CB +:1019B0006FFFDA4E0146243EB068021907EB48007B +:1019C00010440F18284600F0F7FA4FF47A7B00F61F +:1019D000FB01D846B1FBF8F0384400F12009284655 +:1019E000F5F756FFB1680246A9EB0100001B861A05 +:1019F000284600F0E1FA07462846F5F75BFF381A5B +:101A0000B0FBF8F0311A182628234FF4C8774FF4AA +:101A1000BF78082D2CD0042D27D00020022D2AD0ED +:101A20000822283002EB820C0CEB82121044014495 +:101A3000082D28D0042D21D00020022D28D01E46AC +:101A4000082200F5B07000BF02EB0212324490424F +:101A50002AD2082D22D0042D1DD00020022D20D006 +:101A60000822283002EB02122CE040461022D9E76F +:101A700038464022D6E704221830D3E7464640F2E3 +:101A8000EE301022E0E73E464FF435604022DBE7BF +:101A90000422B430D8E740461022E3E7384640221B +:101AA000E0E704221830DDE7082D4DD0042D48D0A2 +:101AB0000020022D4BD0082210F12800C2EBC203F7 +:101AC00003EB421210440E182821284600F08CFA2D +:101AD00006EB4000082D40D0042D3BD00027022DFE +:101AE0003DD0082117F1280701EB811107EB810197 +:101AF000451805F596750C98F5F7DCFE4AF23B5152 +:101B00000144B1FBFBF0854EFB30A6F12407316C9C +:101B100004F1FB020844B9684B191A44824228D9DF +:101B2000621911440D1AFB35E1F7B0F8B9680844A1 +:101B300061190844B0F1807F36D2642D12D264203E +:101B400011E040461022B9E738464022B6E70422A9 +:101B50001830B3E747461021C6E74021C4E7042107 +:101B60001837C1E72846F3F7D4FDE8B1306C2844B4 +:101B70003064E1F78BF8B968293821440844CDE98D +:101B8000000996F839008DF8080002208DF8090048 +:101B90006846FCF7D2FE00B1FFDFFCF7ADFF00B1F5 +:101BA000FFDF5046BDE8FE8F4FF0000AF9E71FB592 +:101BB00000F042FB594C607880B994F82410002260 +:101BC0006846FFF700F938B194F824006946F3F746 +:101BD0009DFA18B9FFDF01E00120E070F2F756FF2F +:101BE00000206074A0741FBD2DE9F84FFDF7B8F90F +:101BF0000646451CC07840090CD001280CD00228AC +:101C00000CD000202978824608064FF4967407D439 +:101C10001E2006E00120F5E70220F3E70820F1E7A7 +:101C20002046B5F80120C2F30C0212FB00F7C809E8 +:101C300001D010B103E01E2401E0FFDF0024FFF714 +:101C400041FDA7EB00092878B77909EB0408C0F338 +:101C5000801010B120B1322504E04FF4FA7501E094 +:101C6000FFDF00250C2F00D3FFDF2D482D4A30F871 +:101C70001700291801FB0821501CB1FBF0F5F4F7FF +:101C8000F9FDF5F717FE4FF47A7100F27160B0FBC1 +:101C9000F1F1A9EB0100471BA7F15900103FB0F586 +:101CA000237F11D31D4E717829B902465346294628 +:101CB0002046FFF787FD00F0BFFAF2F7E7FE0020AD +:101CC0007074B074BDE8F88F3078009053462246A7 +:101CD00029463846FFF762FE0028F3D10121022091 +:101CE000FDF743F9BDE8F84F61E710B50446012957 +:101CF00003D10A482438007830B1042084F8D80091 +:101D0000BDE81040F2F7C2BE00220121204600F0DB +:101D100097F934F8580F401C2080F1E7C4120020D6 +:101D2000A45C02003F420F002DE9F0410746FDF799 +:101D300017F9050000D1FFDF29783846FBF775FC5D +:101D4000F84C0146A4F12406E069B268024467B386 +:101D50002878082803D0042803D000270BE00823A4 +:101D600000E0022303EB430728374FF4A873082849 +:101D700004D0042802D002280FD028233B4408288E +:101D80000DD004280DD002280DD00820C0EBC007CC +:101D900007EB40101844983009E01823EEE7402084 +:101DA000F4E71020F2E70420F0E74FF4FC70104451 +:101DB000471828783F1DF5F77DFD024628784FF437 +:101DC0007A7102281DD001281DD040F6340010443D +:101DD0004AF2EF021044B0FBF1F03A1AA06A40F266 +:101DE000E241B0464788D8304F43316A81420DD036 +:101DF0003946606B00F087F90646B84207D9FFDF25 +:101E000005E00846E3E74FF4C860E0E70026C6486F +:101E10008068864207D2A16A40F271224888424314 +:101E200006EB420604E040F2E240B6FBF0F0A16AA5 +:101E3000C882A06A297880F85410297880F8551053 +:101E400005214175C08A6FF41C71484306EB4000C0 +:101E500040F63541C8F81C00B0EB410F00D3FFDF5E +:101E6000BDE8F08110B5052937D2DFE801F005099A +:101E7000030D3100002100E00121BDE8104034E7EE +:101E8000032180F8D81010BD0446408840F2E2419A +:101E90004843A549091D0860D4F800010089E08283 +:101EA000D4F8000180796075D4F800014089608021 +:101EB000D4F800018089A080D4F80001C089E080B6 +:101EC0002046A16AFFF7C6FB022084F8D80010BDA7 +:101ED000816ABDE81040FFF7BDBBFFDF10BD70B5E4 +:101EE000904C243C0928A1683FD2DFE800F0050BA4 +:101EF0000B15131538380800BDE8704057E6BDE8EB +:101F0000704071E6022803D00020BDE870400BE766 +:101F10000120FAE7E16070BD032802D005281CD03B +:101F200000E0E1605FF0000600F086F97D4D0120E1 +:101F300085F82C0085F83860A86AE9690026C0F8A1 +:101F4000DC1080F8D860E068FFF734FB00B1FFDFF9 +:101F5000F2F79CFD6E74AE7470BD0126E4E7724822 +:101F60000078BDE87040E0F7D7BFFFDF70BD6D4976 +:101F700024394860704770B56A4D0446243DB1B1BC +:101F80004FF47A76012903D0022905D0FFDF70BD16 +:101F90001846F5F7C9FC05E06888401C68801046C3 +:101FA000F5F7A1FC00F2E730B0FBF6F0201AA860CC +:101FB00070BD5C4800787047082803D0042801D021 +:101FC000F5F778BC4EF628307047002804DB00F1A6 +:101FD000E02090F8000405E000F00F0000F1E020A0 +:101FE00090F8140D4009704710F00C0000D008461E +:101FF000704710B50446202800D3FFDF4948483019 +:1020000030F8140010BD70B505460C461046F5F7C3 +:1020100051FC4FF47A71022C0DD0012C0DD040F6FA +:10202000340210444AF247321044B0FBF1F0284425 +:1020300000F2931070BD0A46F3E74FF4C862F0E770 +:102040001FB513460A46044601466846FEF7A5FB3F +:1020500094F8E2006946F3F759F8002800D1FFDF51 +:102060001FBD70B52F4C0025257094F82400F2F7A1 +:10207000E4FD00B9FFDF84F8245070BD2DE9F04184 +:10208000050000D1FFDF274A0024243AD5F8EC6090 +:102090002046631E116A08E08869B04203D3984263 +:1020A00001D203460C460846C9680029F4D104B998 +:1020B00004460021C5F8E840D835C4B1E068E560C1 +:1020C000E86000B105612E698846A96156B1B06922 +:1020D00030B16F69B84200D2FFDFB069C01BA861A0 +:1020E000C6F818800F4D5CB1207820B902E0E96095 +:1020F0001562E8E7FFDF6169606808446863AFE67E +:10210000C5F83480ACE610B50C4601461046F3F72E +:10211000CCFA00280ADA211A491EB1FBF4F101FBBE +:10212000040010BDC41200208401002090FBF4F1D3 +:1021300001FB1400F5E74648016A002001E008466B +:10214000C9680029FBD170477FB504466FF00400D1 +:10215000FFF73BFFC5B21920FFF737FFC0B285423A +:1021600000D0FFDFFCF7FCFE4088C00407D001214F +:102170000320FCF7FAFE37480078E0F7CDFE002296 +:1021800021466846FEF71FFE38B169462046F2F741 +:10219000BDFF002800D1FFDF7FBD2D490120243184 +:1021A000C870FEF715FD7FBD2DE9FE43284D0120C7 +:1021B000287000264FF6FF7420E00621F0F71AFC85 +:1021C000070000D1FFDF97F8E200D837F3F707FBED +:1021D00007F80A6BA14617F8E289B8F1200F00D37F +:1021E000FFDF1B4A6C3222F8189097F8E200F2F7F2 +:1021F00024FD00B9FFDF202087F8E20069460620B1 +:10220000F0F781FB50B1FFDF08E0029830B190F8A1 +:10221000D81019B10088A042CFD104E06846F0F789 +:1022200050FB0028F1D02E70BDE8FE8310B5FFF7FB +:10223000EAFE00F5C87074E705480021243090F8E4 +:10224000392000EB4200C18502480078E0F764BE07 +:10225000A012002084010020012804D0022805D00B +:10226000032808D105E0012907D004E0022904D0A1 +:1022700001E0042901D00020704701207047FE488A +:10228000806890F8881029B1B0F88410B0F88620E2 +:10229000914215D290F88C1029B1B0F88A10B0F89C +:1022A000862091420CD2B0F88220B0F880108A4289 +:1022B00006D290F86820B0F87E001AB1884203D3A5 +:1022C000012070470628FBD2002070472DE9F0411D +:1022D000E94D0746A86800F1580490F8FC0030B9B1 +:1022E000E27B002301212046FAF758FE10B1608DF1 +:1022F000401C608501263D21AFB92878022808D00E +:1023000001280AD06878C8B110F0140F09D01E2037 +:1023100039E0162037E0E6763EE0A86890F8FE0047 +:1023200031E0020701D52177F5E7810701D02A20A6 +:1023300029E0800600D4FFDF232024E094F8300059 +:1023400028B1A08D411CA185E18D884213D294F85B +:10235000340028B1608E411C6186E18D88420AD22A +:10236000618D208D814203D3AA6892F8FC2012B9B6 +:10237000E28D914201D3222005E0217C29B1E18C3C +:10238000814207D308202077C5E7E08C062801D3D7 +:102390003E20F8E7E07EB0B1002020736073207427 +:1023A0000221A868FFF75EFDA86890F8CC1001290B +:1023B00004D1D0F804110878401E0870E878BDE810 +:1023C000F041E0F7A9BDA868BDE8F0410021FFF7A2 +:1023D00049BDA9490C28896881F8CC0014D013287C +:1023E00012D0182810D0002211280ED007280BD0A8 +:1023F00015280AD0012807D0002805D0022803D0CC +:1024000021F8842F012008717047A1F88A207047B5 +:1024100010B5994CA1680A88A1F8462181F84401B9 +:1024200091F8540001F073FBA16881F8480191F81C +:10243000550001F06CFBA16881F84901012081F889 +:102440004201002081F81601E078BDE81040E0F775 +:1024500063BD70B5884C00231946A06890F86420CD +:102460005830FAF79BFD00283DD0A06890F808117D +:102470000025C9B3A1690978B1BB90F86500FAF7E6 +:1024800075FD88BBA168B1F858000A282DD905222E +:102490000831E06903F046F810B3A068D0F80411E1 +:1024A000087858B10522491CE06903F03BF8002880 +:1024B00019D1A068D0F80401007840B9A068E1699A +:1024C000D0F804010A68C0F8012009794171A068B8 +:1024D000D0F804110878401C08700120FFF779FF3C +:1024E000A06880F8085170BDFFE7A06890F80C1153 +:1024F00011B190F80D11B9B390F816110029F2D06E +:1025000090F817110029EED190F86500FAF72EFD2A +:102510000028E8D1A06890F8540001F0F8FA0646C7 +:10252000A06890F8550001F0F2FA0546A06890F80E +:1025300018113046FFF790FE90B3A06890F819117B +:102540002846FFF789FE58B3A268B2F8583092F8CF +:102550005410B2F81A01F832FBF730F818B3A1683A +:10256000252081F86400BEE7FFE790F86510242974 +:1025700017D090F86410242913D0002300F1FA0238 +:1025800000F58671FAF7BAFDA06880F80C5130F8B2 +:10259000421FA0F88C108188A0F88E10142007E04C +:1025A00005E00123EAE7BDE87040002030E716208F +:1025B000BDE870400DE710B5F3F73CFC0C2813D3D1 +:1025C0002D4C0821A068D0F800011E30F3F736FC2E +:1025D00028B1A0680421C030F3F730FC00B9FFDF58 +:1025E000BDE810400320F4E610BD10B5224CA068F1 +:1025F000D0F800110A78002A1FD049880288914239 +:102600001BD190F86420002319465830FAF7C6FC15 +:10261000002812D0A068D0F800110978022907D04C +:1026200003290BD0042917D0052906D108200DE075 +:1026300090F86500FAF79AFC40B110BD90F8691067 +:1026400039B190F86A0000B9FFDF0A20BDE81040F8 +:10265000BFE6BDE81040AEE790F890008007ECD1EF +:102660000C20FFF7B6FEA068002120F8841F01218E +:102670000171017B02E000009001002041F00101A6 +:10268000017310BD70B5FE4CA268556DFAF730FFAE +:10269000EBB2C1B200228B4203D0A36883F8FA10D8 +:1026A00002E0A16881F8FA20C5F30721C0F30720F2 +:1026B000814203D0A16881F8FB0014E7A06880F88C +:1026C000FB2010E770B5EE48806890F84E20448EED +:1026D000C38E418FB0F84050022A23D0A94200D3C4 +:1026E00029460186C18FB0F84220914200D311469D +:1026F0008186018FB0F84420914200D31146418673 +:10270000818FB0F84620914200D31146C186418E98 +:10271000A14200D90C464486C18E994200D90B468D +:10272000C386E0E6028E914200D31146C68F828EA8 +:10273000964200D23246A94200D329460186B0F81B +:1027400042108A4200D30A468286002180F84E1049 +:10275000CFE770B5CA4CA06890F8CC10FE2955D1CF +:102760006178002952D190F8672000230121583068 +:10277000FAF714FC002849D1A06890F8FC1009B1C0 +:10278000022037E090F86420002319465830FAF709 +:1027900005FC28B1A06890F87C0008B1122029E05F +:1027A000A068002590F86420122A1DD004DC032ABA +:1027B00023D0112A04D119E0182A1AD0232A26D0AE +:1027C000002304215830FAF7E9FB00281ED1A06845 +:1027D00090F86510192970D020DC01292AD002292F +:1027E00035D0032932D120E00B2003E0BDE8704052 +:1027F000E1E60620BDE87040EBE510F8CA1F017164 +:102800000720FFF7E6FDA06880F864506BE618200B +:10281000FFF7DFFDA068A0F8845064E61D2918D0FA +:102820001E2916D0212964D148E010F8C91F417132 +:1028300007206EE00C20FFF7CCFDA06820F88A5F2F +:10284000817941F00101817100F8255C51E013208C +:102850002AE090F80D217ABB90F80C21AAB1242926 +:1028600011D090F8641024290DD0002300F1FA0251 +:1028700000F58671FAF742FCA0681E2180F8651009 +:1028800080F80C5103E00123F0E71E2931D1FFF756 +:1028900019FF01F04EF9A06830F8421FA0F88C1023 +:1028A0008188A0F88E101520FFF793FDA068A0F88E +:1028B0008A5000BF80F865501BE029E090F87D1039 +:1028C00049B100F8FA5F45701820FFF782FDA06853 +:1028D000A0F88A500DE090F8171151B990F8161130 +:1028E00039B1016DD0F81801FFF7CCFE1820FFF7C1 +:1028F00070FDA06890F8CC00FE2887D1FFF775FE28 +:10290000A06890F8CC00FE2887D1BDE87040A0E513 +:102910001120FFF75EFDA068CCE7594A01299268B3 +:1029200019D0002302290FD003291ED010B301288B +:102930002BD0032807D192F86400132803D016285F +:1029400001D0182804D1704792F8CC000028FAD0A2 +:10295000D2F8000117E092F8CC000128F3D0D2F8A9 +:1029600004110878401E0870704792F8CC000328C4 +:10297000EED17047D2F80001B2F858108288891A57 +:1029800009B20029F5DB03707047B2F85800B2F8BD +:102990000A11401A00B20028F6DBD2F804010178CF +:1029A000491E0170704770B5044690F86400002518 +:1029B0000C2810D00D282ED1D4F80011B4F85800EE +:1029C0008988401C884226D1D4F84C012C4E0178CD +:1029D00011B3FFDF42E0B4F85800B4F80A11401C0C +:1029E000884218D1D4F80401D0F80110A1604079D0 +:1029F000207302212046F9F7ABFFD4F804010078D8 +:102A000000B9FFDF0121FE20FFF787FF84F8645043 +:102A1000012084F8980066E52188C180D4F800017F +:102A2000D4F84C1140890881D4F80001D4F84C1135 +:102A300080894881D4F80001D4F84C11C08988817C +:102A4000D4F84C010571D4F84C1109200870D4F861 +:102A50004C1120884880F078E0F75EFA012120468A +:102A6000F9F776FF03212046FFF7FCF9B068D0F8AC +:102A700000010078022800D0FFDF0221FE2001E0E3 +:102A800090010020FFF749FF84F864502BE52DE901 +:102A9000F041002603270125FE4CD4F808C088B178 +:102AA0002069C0788CF8CA0005FA00F0C0F3C05065 +:102AB00000B9FFDFA06800F8647F068480F8245026 +:102AC000BDE8F08100239CF8652019460CF1580000 +:102AD000FAF764FA70B160780028F1D12069C17802 +:102AE000A06880F8C91080F86570A0F88A6080F846 +:102AF0008C50E5E76570E3E7F0B5E64C002385B060 +:102B0000A068194690F865205830FAF747FA012571 +:102B100080B1A06890F8640023280ED024280CD03F +:102B20006846F4F7EAFF68B1009801A9C0788DF80B +:102B3000040008E0657005B0F0BD607840F020004A +:102B40006070F8E70021A06803AB162290F86400DB +:102B5000FAF74FFD002670B1A0689DF80C201621F1 +:102B600000F8F42F4170192100F88F1C00F8685C00 +:102B700020F86A6CDFE72069FBF7E7F878B1216994 +:102B8000087900F00702A06880F85020497901F028 +:102B9000070180F8511090F817310BBB03E00020BB +:102BA000FFF775FFC7E790F81631CBB900F1540372 +:102BB0005F78974205D11A788A4202D180F87D5019 +:102BC0000EE000F59F71028821F8022990F850204C +:102BD0000A7190F8510048710D70E078E0F79CF9A7 +:102BE000A068212180F8651080F88C50A0F88A60D8 +:102BF000A1E770B5A74C00231946A06890F865209E +:102C00005830FAF7CBF928B32069FBF783F830B3D3 +:102C1000A5682069FBF77AF82887A5682069FBF783 +:102C200071F86887A5682069FBF772F8A887A5681E +:102C30002069FBF769F8E887A068012590F864101F +:102C40001C2910D090F84E10012912D090F80D11C7 +:102C500079B90BE0607840F00100607043E4BDE8B2 +:102C60007040002013E780F84E5002E090F80C11FD +:102C700019B11E2180F8651012E01D2180F8651041 +:102C800000F58E710288CA82028F0A83428F4A83BE +:102C9000828F8A83C08FC8830D75E078E0F73CF996 +:102CA000A068002120F88A1F85701CE410B5794CBB +:102CB00000230921A06890F864205830FAF76EF9D3 +:102CC00048B16078002805D1A16801F87C0F08732D +:102CD00001F8180C10BD0120607010BD7CB56D4C62 +:102CE00000230721A06890F864205830FAF756F9BD +:102CF00038B36078002826D169462069FBF720F8B0 +:102D00009DF80000002500F02501A06880F89610CD +:102D10009DF8011001F0490180F8971080F8885063 +:102D2000D0F8001100884988814200D0FFDFA068F8 +:102D3000D0F800110D70D0F84C110A7822B1FFDFE5 +:102D400016E0012060707CBD30F8D02BCA80C16FC6 +:102D50000D71C16F009A8A60019ACA60C26F082122 +:102D6000117030F8D01CC06F4180E078E0F7D4F8E3 +:102D7000A06880F864507CBD70B5464C00231946AD +:102D8000A06890F865205830FAF708F9012540B995 +:102D9000A0680023082190F864205830FAF7FEF864 +:102DA00010B36078002820D1A06890F890008007C8 +:102DB00012D42069FAF78AFFA16881F8910020698E +:102DC00030F8052FA1F892204088A1F8940011F85E +:102DD000900F40F002000870A0684FF0000690F8D5 +:102DE0009010C90702D011E0657066E490F8652084 +:102DF000002319465830FAF7D1F800B9FFDFA06870 +:102E000080F8655080F88C50A0F88A60A06890F82F +:102E10006410012906D180F8646080F88860E07849 +:102E2000E0F77AF8A168D1F80001098842888A425F +:102E3000DBD101780429D8D10670E078E0F76CF88E +:102E4000A06890F864100029CFD180F8886034E43D +:102E500070B5104DA86890F864101A2902D00220AD +:102E600068702AE469780029FBD1002480F88D403D +:102E700080F88840D0F8001100884988814200D04D +:102E8000FFDFA868D0F800110C70D0F84C110A7858 +:102E900022B101E090010020FFDF25E090F88E20B4 +:102EA00072B180F88E400288CA80D0F84C110C7143 +:102EB000D0F84C210E2111700188D0F84C010DE0A2 +:102EC00030F8D02BCA80C16F0C71C26F0121117212 +:102ED000C26F0D21117030F8D01CC06F418000F01E +:102EE000A2FEE878E0F718F8A86880F8644018E4D3 +:102EF00070B5FA4CA16891F86420162A01D0132A03 +:102F000002D191F88E2012B10220607009E462783B +:102F1000002AFBD181F8C800002581F88D5081F886 +:102F20008850D1F8000109884088884200D0FFDF2E +:102F3000A068D0F800010078032800D0FFDF03214B +:102F4000FE20FFF7EAFCA068D0F84C110A780AB11D +:102F5000FFDF14E030F8C82BCA8010F8081BC26FDE +:102F60001171C16F0D72C26F0D21117030F8D01C3C +:102F7000C06F418000F057FEE078DFF7CDFFA0681A +:102F800080F8645042E470B5D44C09210023A06855 +:102F900090F864205830FAF701F8002518B120693C +:102FA000007912281ED0A0680A21002390F864201E +:102FB0005830F9F7F3FF18B120690079142814D0BC +:102FC0002069007916281AD1A06890F864101F298A +:102FD00015D180F8645080F88850BDE870401A2000 +:102FE000FFF716BABDE8704060E6A06800F8645FBD +:102FF000058480F82450BDE8704000F09ABD05E4D7 +:1030000070B5B64C2079C00773D020690023052124 +:10301000C578A06890F864205830F9F7BFFF98B1E0 +:10302000062D11D006DC022D0ED0042D0CD0052D5E +:1030300006D109E00B2D07D00D2D05D0112D03D0A1 +:10304000607840F0080060706078002851D12069F5 +:10305000FAF7A0FD00287ED0206900250226C1785D +:10306000891E162977D2DFE801F00B763437472224 +:10307000764D76254A457676763A53506A6D70736A +:10308000A0680023012190F867205830F9F786FFE7 +:1030900008BB2069FAF7E2FDA16881F8FE0007206D +:1030A00081F8670081F88C5081F8885056E0FFF76E +:1030B0006AFF53E0A06890F864100F2901D0667091 +:1030C0004CE0617839B980F86950122180F86410B9 +:1030D00044E000F0D3FD41E000F0AFFD3EE0FAF740 +:1030E00072FE03283AD12069FAF771FEFFF700FF5C +:1030F00034E03BE00079F9E7FFF7AAFE2EE0FFF7A6 +:103100003BFE2BE0FFF7EAFD28E0FFF7CFFD25E0CF +:10311000A0680023194690F865205830F9F73EFF63 +:10312000012110B16078C8B901E0617016E0A068B3 +:1031300020F88A5F817000F8256C0FE00BE0FFF744 +:1031400058FD0BE000F03CFD08E0FFF7D5FC05E082 +:1031500000F002FD02E00020FFF799FCA268F2E90E +:103160002A01401C41F10001C2E9000153E42DE9AC +:10317000F0415A4C2079800741D5607800283ED133 +:10318000E06801270026C17820461929856805F1E5 +:1031900058006FD2DFE801F04B3E0D6FC1C1801CBB +:1031A00034C1556287C1C1C1C1BE8B9598A4B0C15D +:1031B000BA0095F8672000230121F9F7EFFE0028F7 +:1031C0001DD1A068082180F8671080F8886090E021 +:1031D000002395F865201946F9F7E0FE10B1A068C4 +:1031E00080F88C60A0680023194690F8642058305D +:1031F000F9F7D4FE002802D0A06880F888605FE468 +:10320000002395F864201946F9F7C8FE00B9FFDFDE +:10321000042008E0002395F864201946F9F7BEFE63 +:1032200000B9FFDF0C20A16881F8640048E40023A6 +:1032300095F864201946F9F7B1FE00B9FFDF0D20BB +:10324000F1E7002395F864201946F9F7A7FE00B9C5 +:10325000FFDFA0680F2180F88D7008E095F864000A +:10326000122800D0FFDFA068112180F88E7080F84E +:10327000641025E451E0002395F864201946F9F71D +:103280008DFE20B9A06890F88E0000B9FFDFA0681D +:10329000132180F88D70EAE795F86400182800D0B3 +:1032A000FFDF1A20BFE7BDE8F04100F066BD002354 +:1032B00095F864201946F9F771FE00B9FFDF052083 +:1032C000B1E785F88C6014E4002395F86420194672 +:1032D000F9F764FE00B9FFDF1C20A4E7900100208D +:1032E000002395F865201946F9F758FE00B9FFDF6D +:1032F000A06880F88C6082E7002395F86420194666 +:10330000F9F74CFE00B9FFDF1F208CE7BDE8F04164 +:1033100000F0FBBC85F86560D3E7FFDF6FE710B511 +:10332000F74C6078002837D1207940070FD5A06886 +:1033300090F86400032800D1FFDFA06890F86710C0 +:10334000072904D101212170002180F86710FFF7BF +:103350000EFF00F0B8FCFFF753FEA078000716D56B +:10336000A0680023052190F864205830F9F716FE74 +:1033700050B108206070A068D0F84C1108780D2872 +:1033800000D10020087002E00020F8F73BFAA068A6 +:10339000BDE81040FFF707BB10BD2DE9F041D84C48 +:1033A00007464FF000056078084360702079810679 +:1033B0002046806802D5A0F87E5004E0B0F87E1068 +:1033C000491CA0F87E1000F01AFD0126F8B1A08873 +:1033D000000506D5A06890F86A1011B1A0F87650E3 +:1033E00015E0A068B0F87610491CA0F8761000F03F +:1033F000F5FCA068B0F87610B0F87820914206D3BA +:10340000A0F8765080F82261E078DFF785FD20791A +:1034100010F0600F08D0A06890F8681021B980F80B +:1034200068600121FEF71EFD1FB9FFF778FFFFF767 +:1034300090F93846FEF74AFFBDE8F041F4F76CBB5F +:10344000AF4A51789378194313D1114601288968FE +:1034500008D01079400703D591F86700072808D0F5 +:1034600001207047B1F84800098E884201D8FEF764 +:103470008BB900207047A249C2788968012A06D01A +:103480005AB1182A08D1B1F8F810FAF77ABCB1F895 +:103490000A114172090A81727047D1F800118988B6 +:1034A0004173090A8173704770B5954C05460E4605 +:1034B000A0882843A080A80703D5E80700D0FFDF35 +:1034C000E660E80700D02661A80719D5F07806283D +:1034D00002D00B2814D10BE0A06890F864101829D2 +:1034E0000ED10021E0E92A11012100F83E1C07E07D +:1034F000A06890F86410122902D1002180F86A10A7 +:10350000280601D50820A07068050AD5A068828821 +:10351000B0F85810304600F081FC3046BDE87040ED +:10352000A9E762E43EB505466846F4F7C0FA00B97B +:10353000FFDF2221009802F0A0F803210098FAF79B +:1035400011FB0098017821F0100101702946FAF76B +:103550002EFB6B4C192D71D2DFE805F020180D3EC3 +:10356000C8C8C91266C8C9C959C8C8C8C8BBC9C96A +:1035700071718AC89300A168009891F8FD1003E06A +:10358000A168009891F8CE100171B0E0A068D0F861 +:1035900004110098491CFAF756FBA8E0A1680098AE +:1035A000D1F8002192790271D1F80021128942717B +:1035B000120A8271D1F800215289C271120A027274 +:1035C000D1F8002192894272120A8272D1F8001158 +:1035D000C989FAF70FFB8AE0A068D0F800110098BB +:1035E000091DFAF73DFBA068D0F8001100980C31D6 +:1035F000FAF740FBA068D0F8001100981E31FAF7E6 +:103600003FFBA1680098C031FAF748FB6FE06269A0 +:1036100000981178017191884171090A817151886E +:10362000C171090A017262E03649D1E90001CDE9B0 +:10363000010101A90098FAF74BFB58E056E0A06899 +:10364000B0F840100098FAF755FBA068B0F8CE101B +:103650000098FAF753FBA068B0F844100098FAF706 +:1036600041FBA068B0F8D0100098FAF73FFB3EE0AD +:10367000A268009892F81811017192F8191141711D +:1036800035E0A06890F8FB00F9F729FF01460098A3 +:10369000FAF773FBA06890F8FA0000F033FA70B103 +:1036A000A06890F8540000F02DFA40B1A06890F89E +:1036B000FA1090F85400814201D0002002E0A06886 +:1036C00090F8FA00F9F70BFF01460098FAF751FB62 +:1036D0000DE0A06890F8F5100098FAF772FBA0686A +:1036E00090F8F4100098FAF770FB00E0FFDFF4F7B1 +:1036F000F1F900B9FFDF0098FFF7BDFE3EBD000005 +:1037000090010020BC5C0200F948806890F8FA1033 +:1037100009B990F8541080F8541090F8FB1009B9CA +:1037200090F8551080F855100020FEF771BEF8B5DE +:10373000EF4E00250446B060B5807570B5703570E9 +:103740000088F4F7B1F9B0680088F4F7D3F9B4F859 +:10375000E000B168401C82B201F15800F9F7D5F9D8 +:1037600000B1FFDF94F86500242809D1B4F858109F +:10377000B4F8F800081A00B2002801DB707830B104 +:1037800094F8640024280AD0252808D015E0FFF713 +:10379000BBFF84F86550B16881F87D500DE0B4F846 +:1037A0005810B4F8F800081A00B2002805DB707849 +:1037B00018B9FFF7A9FF84F86450A4F8E050FEF7A9 +:1037C0005EFD00281CD1B06890F8CC00FE2801D026 +:1037D000FFF7A8FEC7480090C74BC84A21462846B5 +:1037E000F7F766FFB0680023052190F86420583091 +:1037F000F9F7D4FB002803D0BDE8F840F7F7F3BC95 +:10380000F8BD10B5FEF73BFD20B10020BDE810402B +:103810000146C2E5BDE81040F7F7D0BF70B50C46D1 +:10382000064615464FF4A871204601F048FF268051 +:1038300005B9FFDF2868C4F800016868C4F804010E +:10384000A868C4F84C0191E4EFF7DDB92DE9F04127 +:103850000D4607460621EFF7CDF8041E3DD0D4F8FB +:103860004C110026087858B14A8821888A4207D12D +:1038700009280FD00E2819D00D2826D008283ED0B0 +:1038800094F82201D0B36E701020287084F8226161 +:10389000AF809FE06E7009202870D4F84C01416819 +:1038A00069608168A9608089A88133E00846EFF7E4 +:1038B000D3F90746EEF77FFE70B96E700E202870C0 +:1038C000D4F84C014068686011E00846EFF7C4F98D +:1038D0000746EEF770FE08B1002090E46E700D20F0 +:1038E0002870D4F84C014168696000892881D4F8B7 +:1038F0004C0106703846EEF758FE6BE00EE06E7035 +:1039000008202870D4F84C01416869608168A9607A +:10391000C068E860D4F84C0106705BE094F82401BC +:10392000A0B16E70152028700BE000BF84F82461F0 +:10393000D4F826016860D4F82A01A860B4F82E01F2 +:10394000A88194F824010028F0D143E094F83001D4 +:1039500070B16E701D20287084F83061D4F8320187 +:103960006860D4F83601A860B4F83A01A88131E063 +:1039700094F83C0140B16E701E20287084F83C61C0 +:10398000D4F83E01686025E094F81C0170B16E70B7 +:103990001B20287005E000BF84F81C61D4F81E01CC +:1039A000686094F81C010028F6D113E094F84201F5 +:1039B000002892D06E701620287007E084F84261CB +:1039C000D4F844016860B4F84801288194F84201B1 +:1039D0000028F3D1012012E4454A5061D1707047AC +:1039E00070B50D4604464EE0B4F8E000401CA4F863 +:1039F000E000B4F87E00401CA4F87E00204600F0F1 +:103A0000FEF9B8B1B4F87600401CA4F87600204660 +:103A100000F0E4F9B4F87600B4F87810884209D3DD +:103A20000020A4F87600012084F822013048C078F4 +:103A3000DFF772FA94F8880020B1B4F88400401CD3 +:103A4000A4F8840094F88C0020B1B4F88A00401CDB +:103A5000A4F88A0094F8FC0040B994F86720002389 +:103A6000012104F15800F9F799FA20B1B4F8820065 +:103A7000401CA4F882002046FEF795FFB4F85800D9 +:103A8000401CA4F858006D1EADB2ADD249E5184AED +:103A9000C2E90601704770B50446B0F87E0094F89C +:103AA0006810D1B1B4F880100D1A2D1F94F87C0065 +:103AB00040B194F864200023092104F15800F9F77B +:103AC0006DFA70B1B4F87660204600F098F938B11C +:103AD000B4F87800801B001F03E0C0F10205E5E7A1 +:103AE0002846A84200DA0546002D09DC002018E52A +:103AF000900100209B33020041340200A9340200EF +:103B0000A8B20EE510F00C0000D00120704710B5EF +:103B1000012808D0022808D0042808D0082806D098 +:103B2000FFDF204610BD0124FBE70224F9E7032450 +:103B3000F7E710B5EF4C0421A068FEF793F9A068F1 +:103B400090F84E10012903D0BDE8104000F098B95C +:103B5000022180F84E1010BD70B5E64CA06890F8B8 +:103B600064001F2804D0607840F001006070D8E441 +:103B70002069FAF7F4F8D8B1206901220179407977 +:103B800001F0070161F30705294600F0070060F323 +:103B90000F21A06880F888200022A0F8842023222A +:103BA00000F8642FD0F8B400BDE87040FEF76ABD9D +:103BB0000120FEF76CFFBDE870401E20FEF728BC18 +:103BC00070B5CC4C00230A21A06890F864205830CE +:103BD000F9F7E4F910B32069FAF79CF8A8B1A568E1 +:103BE0002069FAF793F82887A5682069FAF78AF818 +:103BF0006887A5682069FAF78BF8A887A568206907 +:103C0000FAF782F8E887FEF75DFDA168002081F8E9 +:103C1000880081F86400BDE870408AE7607840F071 +:103C2000010060707DE4B34810B580680088EFF74C +:103C300013F8BDE81040EEF7A9BC10B5AD4CA36871 +:103C400093F86400162802D00220607010BD6078DE +:103C50000028FBD1D3F80001002200F11E010E3034 +:103C6000B033F9F715F9A0680021C0E92811012146 +:103C700080F86910182180F8641010BD10B59D4CB3 +:103C8000A06890F86410132902D00220607010BD63 +:103C900061780029FBD1D0F8001100884988814261 +:103CA00000D0FFDFA068D0F8001120692631FAF7B4 +:103CB00002F8A1682069C431FAF705F8A168162056 +:103CC00081F8640010BD10B58A4C207900071BD51F +:103CD0006078002818D1A068002190F8CC00FEF789 +:103CE0001CFEA06890F8CC00FE2800D1FFDFA06881 +:103CF000FE2180F8CC1090F86710082904D1022129 +:103D00002170002180F8671010BD70B5794D242115 +:103D10000024A86890F86520212A05D090F8642036 +:103D2000232A18D0FFDF8EE590F8FA2012B990F818 +:103D3000FB202AB180F86510A86880F88C4082E5E5 +:103D400000F8654F047690F8B1000028F4D0002008 +:103D5000FEF75EFBF0E790F8FA2012B990F8FB202E +:103D60002AB180F86410A86880F888406BE580F874 +:103D700064400020FEF74CFBF5E770B55D4C002574 +:103D8000A068D0F8001103884A889A4218D10978AF +:103D9000042915D190F86420002319465830F9F70A +:103DA000FDF800B9FFDFA06890F89010890703D4F0 +:103DB000012180F8641003E000F8885F806F0570CF +:103DC000A0680023194690F865205830F9F7E6F806 +:103DD000002802D0A06880F88C5034E5B0F8782034 +:103DE000B0F876108A4201D3511A00E0002182888F +:103DF000521D8A4202D3012180F87C10704710B511 +:103E000090F86A1041B990F86420002306215830D8 +:103E1000F9F7C4F8002800D0012010BD70B5114496 +:103E2000344D891D8CB2C078A968012806D040B1F4 +:103E3000182805D191F8FA0038B109E0A1F80A4133 +:103E400001E5D1F800018480FDE491F8FB1091B107 +:103E5000FFF758FE80B1A86890F85400FFF752FEB3 +:103E600050B1A86890F8FA1090F85420914203D00D +:103E700090F8FB0000B90024A868A0F8F840E2E43C +:103E80002DE9F0411B4DA86800F58E740188618111 +:103E9000018EA181818EE181018FB0F84420914291 +:103EA00000D311462182828FB0F846108A4200D298 +:103EB0001146618290F85500FFF724FE4FF4296700 +:103EC00028B1608A3E46B84200D906466682A86894 +:103ED00090F85400FFF716FE20B1E089B84200D9EF +:103EE0000746E78101202072E878BDE8F041DFF75E +:103EF00013B800009001002070B58D4C0829207A7D +:103F000062D2DFE801F0041959592561615978B18D +:103F1000F2F73CFD01210846F2F7DFFEF3F713FD4F +:103F20000020A072F2F7E5FDBDE87040F3F766B837 +:103F3000BDE87040F0F7AABDD4E90001F0F79DFBA1 +:103F40002060A07A401CC0B2A07228281CD370BD8B +:103F5000A07A0025401EC6B2E0683044F3F73FF96E +:103F600010B9E1687F208855A07A272828BF01254D +:103F70002846F3F751FCA07A282809D2401CC0B289 +:103F8000A072282828BF70BDBDE87040F2F7B1BD0F +:103F9000207A00281CBF012000F085F8F2F7A0FF6E +:103FA000F3F71EF80120E07262480078DEF7B4FFF4 +:103FB000BDE87040F0F76ABD002808BF70BD002062 +:103FC000BDE8704000F06FB8FFDF70BD10B5584C11 +:103FD000207A002804BF0C2010BD00202072E0725F +:103FE000607AF1F7AEF9607AF1F7F9FB607AF0F7F1 +:103FF00024FE00280CBF1F20002010BD002270B539 +:104000004B4C06460D46207A68B12272E272607A05 +:10401000F1F797F9607AF1F7E2FB607AF0F70DFEBD +:10402000002808BFFFDF4348E560067070BD70B52B +:10403000050007D0A5F5E8503F494C3881429CBFA8 +:10404000122070BD3A4CE068002804BF092070BD02 +:10405000207A00281CBF0C2070BD3848F0F791FD75 +:104060006072202804BF1F2070BDF0F705FE20609D +:10407000002D1CBF284420600120656020720020B4 +:1040800000F011F8002070BD2949CA7A002A04BF47 +:10409000002070471F22027000224270CB684360EC +:1040A000CA72012070472DE9F04184B00746F0F74D +:1040B000E3FD1F4D8046414668682C6800EB800098 +:1040C00046002046F1F7F1FAB04206DB6868811B32 +:1040D0004046F0F7D2FA0446286040F23476214692 +:1040E0004046F1F7E2FAB04204DA31464046F0F7D2 +:1040F000C4FA044600208DF800004FF4DD60039000 +:1041000004208DF80500002F14BF012003208DF836 +:10411000040068460294F0F77EFF687A6946F0F77B +:10412000F5FF002808BFFFDF04B0BDE8F081000004 +:104130004C130020B0010020B5EB3C00F93E02001A +:104140002DE9F0410C4612490D68114A1149083217 +:104150001160A0F12001312901D301200CE0412898 +:1041600010D040CC0C4F94E80E0007EB8000241FC9 +:1041700050F8807C3046B84720600548001D056037 +:10418000BDE8F0812046DDF743F8F5E706207047EB +:104190001005024001000001C45C020010B5534844 +:1041A000F1F7CAFD00B1FFDF5048401CF1F7C4FD34 +:1041B000002800D0FFDF10BD2DE9F14F4C4ED6F89E +:1041C00000B001274948F1F7BFFDDFF8208128B989 +:1041D0005FF0000708F10100F1F7CCFD454C002528 +:1041E0004FF0030901206060C4F80051C4F8045185 +:1041F000009931602060DFF800A118E0DAF80000D3 +:10420000C00614D50E2000F064F8EFF3108010F013 +:10421000010072B600D00120C4F80493D4F8001154 +:1042200019B9D4F8041101B920BF00B962B6D4F8A5 +:10423000000118B9D4F804010028DFD0D4F8040133 +:104240000028CFD137B1C6F800B008F10100F1F76E +:104250007BFD11E008F10100F1F776FD0028B9D1EE +:10426000C4F80893C4F80451C4F800510E2000F0BB +:1042700030F81E48F1F77EFD0020BDE8F88F2DE9EB +:10428000F0438DB00D46064600240DF110090DF1E6 +:10429000200817E004EB4407102255F82710684661 +:1042A00001F06CF905EB870710224846796801F0A8 +:1042B00065F96846FFF780FF10224146B86801F0B3 +:1042C0005DF9641CB442E5DB0DB00020BDE8F0836D +:1042D00072E7002809DB00F01F020121914040092C +:1042E000800000F1E020C0F880127047B10100208A +:1042F00004E5004000E0004010ED00E0B14900207E +:104300000870704770B5B04D01232B60AF4B1C682F +:10431000002CFCD0002407E00E6806601E68002E0A +:10432000FCD0001D091D641C9442F5D300202860B8 +:1043300018680028FCD070BD70B5A24E0446A44D8C +:104340003078022800D0FFDFAC4200D3FFDF716974 +:10435000A048012903D847F23052944201DD0322DC +:104360004271491C7161291BC1609A497078F0F74C +:10437000CDFE002800D1FFDF70BD70B5914C0D4619 +:104380006178884200D0FFDF914E082D4BD2DFE8E4 +:1043900005F04A041E2D4A4A4A382078022800D0E7 +:1043A000FFDF03202070A078012801D020B108E0B1 +:1043B000A06800F039FE04E004F1080007C8FFF728 +:1043C000A1FF05202070BDE87040F0F75FBBF0F75B +:1043D00053FC01466068F1F768F9B04202D26169A6 +:1043E00002290BD30320F1F746FC12E0F0F744FC5E +:1043F00001466068F1F759F9B042F3D2BDE8704068 +:104400009AE7207802280AD0052806D0FFDF04208A +:104410002070BDE8704000F0CAB8022000E0032020 +:10442000F1F729FCF3E7FFDF70BD70B50546F0F743 +:1044300023FC644C60602078012800D0FFDF6549D0 +:10444000012008700020087104208D6048716048C8 +:10445000C860022020706078F0F758FE002800D174 +:10446000FFDF70BD10B5574C207838B90220F1F746 +:1044700018FC18B90320F1F714FC08B1112010BD85 +:104480005548F0F77EFB6070202804D00120207092 +:104490000020606110BD032010BD2DE9F0471446D7 +:1044A000054600EB84000E46A0F1040800F0CFFDA5 +:1044B00007464FF0805001694F4306EB8401091F06 +:1044C000B14201D2012100E0002189461CB10069FE +:1044D000B4EB900F02D90920BDE8F0872846DCF73D +:1044E000EBFE90B9A84510D3BD4205D2B84503D222 +:1044F00045EA0600800701D01020EDE73046DCF7E2 +:10450000DBFE10B9B9F1000F01D00F20E4E733480A +:1045100033490068884205D0224631462846FFF7D5 +:10452000F1FE14E0FFF79EFF0028D5D125480021B9 +:104530008560C0E90364817000F06FF810B14FF43A +:10454000A97000E0292060431830FFF76EFF0020BB +:10455000C2E770B505464FF0805004696C432046B1 +:10456000DCF7AAFE08B10F2070BD00F070FDA84274 +:1045700001D8102070BD194819490068884203D03D +:10458000204600F051FD10E0FFF76CFF0028F1D14C +:104590000C4801218460817000F03FF808B1114897 +:1045A00000E011481830FFF740FF002070BD10B543 +:1045B000044C6078F0F741FB00B9FFDF0020207069 +:1045C00010BD0000B401002004E5014000E40140FA +:1045D000105C0C005C1300207B43020054000020A0 +:1045E000BEBAFECA645E0100084C01004FF0805064 +:1045F000D0F830010A2801D0002070470120704710 +:1046000000B5FFF7F3FF20B14FF08050D0F8340130 +:1046100008B1002000BD012000BD4FF08050D0F84F +:104620003011062905D0D0F83001401C01D00020FF +:104630007047012070474FF08050D0F830010828B3 +:1046400001D0002070470120704700B5FFF7E5FF5B +:1046500048B14FF08050D0F83411062905D3D0F876 +:104660003401401C01D0002000BD012000BD00B578 +:10467000FFF7D3FF58B14FF08050D0F8341106291E +:1046800005D3D0F83401401C01D0012000BD00202A +:1046900000BD00007B49096801600020704779492E +:1046A00008600020704701218A0720B1012804D04A +:1046B00042F204007047916700E0D1670020704724 +:1046C00071490120086042F20600704708B50423D2 +:1046D0006D4A1907103230B1C1F80433106840F048 +:1046E000010010600BE0106820F001001060C1F8BC +:1046F00008330020C1F808016448006800900020D9 +:1047000008BD011F0B2909D85F4910310A6822F042 +:104710001E0242EA400008600020704742F2050095 +:1047200070470F2809D8584910310A6822F470627E +:1047300042EA002008600020704742F205007047FE +:10474000000100F18040C0F804190020704700010A +:1047500000F18040C0F8081900207047000100F106 +:104760008040D0F80009086000207047012801D976 +:1047700007207047464A52F8200002680A43026048 +:1047800000207047012801D907207047404A52F89D +:10479000200002688A43026000207047012801D986 +:1047A000072070473A4A52F820000068086000204D +:1047B0007047020037494FF0000003D0012A01D0B2 +:1047C000072070470A607047020033494FF000002D +:1047D00003D0012A01D0072070470A60704708B54E +:1047E0004FF40072510510B1C1F8042308E0C1F87C +:1047F00008230020C1F8240124481C3000680090E0 +:10480000002008BD08B58022D10510B1C1F80423ED +:1048100008E0C1F808230020C1F81C011B4814302F +:1048200000680090002008BD08B54FF48072910523 +:1048300010B1C1F8042308E0C1F808230020C1F832 +:1048400020011248183000680090002008BD0D4972 +:10485000383109680160002070474FF08041002026 +:10486000C1F80801C1F82401C1F81C01C1F82001F8 +:104870004FF0E020802180F800140121C0F80011E1 +:10488000704700000004004000050040080100409F +:10489000885D020078050040800500406249634B56 +:1048A0000A6863499A42096801D1C1F310010160A5 +:1048B000002070475C495D4B0A685D49091D9A42BA +:1048C00001D1C0F310000860002070475649574BD3 +:1048D0000A68574908319A4201D1C0F310000860B4 +:1048E0000020704730B5504B504D1C6842F2080311 +:1048F000AC4202D0142802D203E0112801D318469A +:1049000030BDC3004B481844C0F81015C0F814253A +:10491000002030BD4449454B0A6842F209019A42E1 +:1049200002D0062802D203E0042801D308467047CB +:10493000404A012142F83010002070473A493B4B71 +:104940000A6842F209019A4202D0062802D203E024 +:10495000042801D308467047364A012102EBC00003 +:1049600041600020704770B52F4A304E314C1568B9 +:1049700042F2090304EB8002B54204D0062804D2B7 +:10498000C2F8001807E0042801D3184670BDC1F32F +:104990001000C2F80008002070BD70B5224A234EF6 +:1049A000244C156842F2090304EB8002B54204D09E +:1049B000062804D2D2F8000807E0042801D31846DC +:1049C00070BDD2F80008C0F310000860002070BD70 +:1049D000174910B50831184808601120154A002100 +:1049E00002EBC003C3F81015C3F81415401C1428BB +:1049F000F6D3002006E0042804D302EB8003C3F8BA +:104A0000001807E002EB8003D3F80048C4F3100459 +:104A1000C3F80048401C0628EDD310BD04490648E1 +:104A2000083108607047000054000020BEBAFECA7A +:104A300000F5014000F001400000FEFF834B1B68C1 +:104A400003B19847BFF34F8F81480168814A01F451 +:104A5000E06111430160BFF34F8F00BFFDE710B568 +:104A6000EFF3108010F0010F72B601D0012400E0C6 +:104A7000002400F0E1F850B1DCF7BEFCEFF7C1FE16 +:104A8000F1F79BF8E7F75EFA73490020086004B974 +:104A900062B6002010BD2DE9F0410C460546EFF34B +:104AA000108010F0010F72B601D0012600E0002640 +:104AB00000F0C2F820B106B962B60820BDE8F08166 +:104AC000DCF78EFBDCF79CFC024600200123470943 +:104AD000BF0007F1E02700F01F01D7F80071CF40B9 +:104AE000F9071BD0202803D222FA00F1C90727D1E9 +:104AF00041B2002904DB01F1E02191F8001405E046 +:104B000001F00F0101F1E02191F8141D4909082974 +:104B100016D203FA01F717F0EC0F11D0401C6428ED +:104B2000D5D3E7F7EDF94D4A4D490020E7F730FAC4 +:104B300049494C4808602046DCF7C5FB60B904E0F1 +:104B400006B962B641F20100B8E7404804602DB1F1 +:104B50002846DCF705FC18B110242CE0424D19E082 +:104B60002878022802D94FF4805424E00724002832 +:104B7000687801D0F8B908E0E8B120281BD8A878F7 +:104B8000212818D8012816D001E0A87898B9E8782B +:104B90000B2810D83549802081F8140DDCF730FC43 +:104BA0002946F0F7F0FFEFF7EBFD00F083FA284617 +:104BB000DCF7F4FB044606B962B61CB1FFF74FFF01 +:104BC00020467BE7002079E710B5044600F034F872 +:104BD00000B101202070002010BD25490860002090 +:104BE000704770B50C4623490D682249224E0831A2 +:104BF0000E60102807D011280CD012280FD01328CF +:104C000011D0012013E0D4E90001FFF744FF35463D +:104C100020600DE0FFF723FF0025206008E02068FA +:104C2000FFF7D2FF03E012492068086000202060EF +:104C30001048001D056070BD07480A490068884299 +:104C400001D101207047002070470000CC010020F6 +:104C50000CED00E00400FA0554000020F8130020D9 +:104C600000000020BEBAFECA905D02000BE000E02A +:104C700004000020100502400100000100B59B491E +:104C800002282ED021DC10F10C0F08BFF42028D010 +:104C90000FDC10F1280F08BFD82022D010F1140F1C +:104CA00008BFEC201DD010F1100F08BFF02018D065 +:104CB00021E010F1080F08BFF82012D010F1040F06 +:104CC0000CBFFC2000280CD015E0A0F10300062842 +:104CD00011D2DFE800F00E0C0A080503082000E0FE +:104CE0000720086000BD0620FBE70520F9E7042047 +:104CF000F7E70320F5E7FFDF00BD00B57C49012899 +:104D000008BF03200CD0022808BF042008D00428C4 +:104D100008BF062004D0082816BFFFDF052000BD0D +:104D2000086000BD70B505460C4616461046F2F701 +:104D3000C1FD022C08BF4FF47A7105D0012C0CBFC5 +:104D40004FF4C86140F6340144183046F2F7ECFDE8 +:104D5000204449F6797108444FF47A71B0FBF1F0C0 +:104D6000281A70BD70B505460C460846F2F7BBFD23 +:104D7000022C08BF40F24C4105D0012C0CBF40F67C +:104D800034014FF4AF5149F6CA62511A08444FF446 +:104D90007A7100F2E140B0FBF1F0281A801E70BD7C +:104DA00070B5064615460C460846F2F79CFD022DE6 +:104DB00008BF4FF47A7105D0012D0CBF4FF4C861C4 +:104DC00040F63401022C08BF40F24C4205D0012CC1 +:104DD0000CBF40F634024FF4AF52891A084449F62A +:104DE000FC6108444FF47A71B0FBF1F0301A70BDE9 +:104DF00070B504460E460846F2F75CFD054630469F +:104E0000F2F792FD28444AF2AB3108444FF47A712C +:104E1000B0FBF1F0201A801E70BD2DE9F04107466D +:104E20001E460D4614461046082A16BF04284EF6A4 +:104E30002830F2F73FFD07EB4701C1EBC71100EB4C +:104E4000C100022D08BF40F24C4105D0012D0CBF1E +:104E500040F634014FF4AF5147182846F2F743FDAE +:104E6000381A4FF47A7100F6B730B0FBF1F52046EE +:104E7000F2F70EFD28443044401DBDE8F08170B5C6 +:104E8000054614460E460846F2F714FD05EB4502AA +:104E9000C2EBC512C0EBC2053046F2F745FD2D1A34 +:104EA0002046082C16BF04284EF62830F2F702FDE3 +:104EB00028444FF47A7100F6B730B0FBF1F5204684 +:104EC000F2F7E6FC2844401D70BD0A49082818BFC7 +:104ED0000428086803BF20F46C5040F4444040F0BC +:104EE000004020F000400860704700000C150040B2 +:104EF00010150040401700402DE9FE430C46804647 +:104F0000F8F744FE074698F80160204601A96A4672 +:104F1000EDF72DFB05000DD0012F02D00320BDE8D9 +:104F2000FE83204602AA0199EDF743FA0298B0F8F1 +:104F300003000AE0022F14D1042E12D3B8F80300A4 +:104F4000BDF80020011D914204D8001D80B2A919AE +:104F5000814202D14FF00000E1E702D24FF00100A0 +:104F6000DDE74FF00200DAE7C2790D2341B342BB1F +:104F70008188012904D94908818004BF01228280E7 +:104F80000168012918BF002930D001686FEA0101CA +:104F9000C1EBC10202EB011281796FEA010101EB61 +:104FA0008103C3EB811111444FEA91420160818872 +:104FB000B2FBF1F301FB132181714FF0010102E01B +:104FC0001AB14FF00001C17170478188FF2908D2E2 +:104FD0004FF6FF7202EA41018180FF2984BFFF2260 +:104FE00082800168012918BF0029CED10360CCE777 +:104FF000817931B1491E11F0FF0181711CBF002080 +:1050000070470120704710B50121C1718171818005 +:1050100004460421F0F712FF002818BF10BD2068D5 +:10502000401C206010BD00000B4A022111600B499A +:105030000B68002BFCD0084B1B1D1860086800286B +:10504000FCD00020106008680028FCD070474FF0AA +:10505000805040697047000004E5014000E40140D1 +:1050600002000B464FF00000014620D0012A04D078 +:10507000022A04D0032A0DD103E0012002E002201D +:1050800015E00320072B05D2DFE803F00406080A29 +:105090000C0E100007207047012108E0022106E0F5 +:1050A000032104E0042102E0052100E00621EFF7DE +:1050B00086BD0000F9480521817000210170417012 +:1050C0007047F7490A78012A05D0CA681044C860B9 +:1050D0004038F0F7B7BA8A6810448860F8E70028CB +:1050E00019D00378EF49F04A13B1012B0ED011E02B +:1050F0000379012B00D06BB943790BB1012B09D196 +:105100008368643B8B4205D2C0680EE00379012BB3 +:1051100002D00BB10020704743790BB1012BF9D1BC +:10512000C368643B8B42F5D280689042F2D801207C +:105130007047DB4910B501220A700279A2B1002242 +:105140000A71427992B104224A718268D34C523278 +:105150008A60C0681434C8606060EFF78DFDCF4985 +:1051600020600220887010BD0322E9E70322EBE7EC +:1051700070B5CB4D044600202870207988B10020FE +:105180002871607978B10420C44E6871A168F06814 +:10519000EFF773FAA860E0685230E8600320B0705F +:1051A00070BD0120ECE70320EEE72DE9F041054654 +:1051B0000226F0F773F9006800B1FFDFB74C012752 +:1051C0003DB12878B0B1012805D0022810D00328BD +:1051D00013D027710CE06868C82807D3F0F799FA54 +:1051E00020B16868FFF76DFF012603E0002601E0AB +:1051F00000F05EF93046BDE8F08120780028F7D154 +:105200006868FFF76CFF0028E3D06868017879B11F +:10521000A078042800D0FFDF01216868FFF7A8FF0D +:105220009F49E078EFF772FF0028E1D1FFDFDFE769 +:10523000FFF77FFF6770DBE72DE9F047974C884663 +:10524000E178884200D0FFDFDFF850920025012787 +:10525000934E09F11409B8F1080F75D2DFE808F090 +:10526000040C28527A808D95A078032802D0022859 +:1052700000D0FFDFBDE8F087A078032802D0022825 +:1052800000D0FFDF0420A07025712078002878D19D +:10529000FFF717FF3078012806D0B068E06000F013 +:1052A00027F92061002060E0E078EFF72CFEF5E7B9 +:1052B000A078032802D0022800D0FFDF2078002841 +:1052C0006DD1A078032816D0EFF7D6FC01464F46E3 +:1052D000D9F80000F0F7E9F900280EDB796881427F +:1052E0000BDB081AF0606E49E078EFF70FFF00283B +:1052F000C0D1FFDFBEE7042028E00420F0F7BBFCAC +:10530000A570B7E7A078032802D0022800D0FFDFFD +:10531000207888BBA078032817D0EFF7ADFC0146B2 +:105320004F46D9F80000F0F7C0F90028E5DB7968AE +:105330008142E2DB081AF0605949E078EFF7E6FEB7 +:10534000002897D1FFDF95E740E00520F0F793FCB8 +:10535000A7708FE7A078042800D0FFDF022004E0C8 +:10536000A078042800D0FFDF0120A1688847FFF75C +:105370001CFF054630E004E011E0A078042800D0CE +:10538000FFDFBDE8F04700F093B8A078042804D010 +:10539000617809B1022800D0FFDF207818B1BDE89C +:1053A000F04700F08EB8207920B10620F0F763FCBA +:1053B0002571CDE7607838B13949E078EFF7A6FE7E +:1053C00000B9FFDF657055E70720BFE7FFDF51E752 +:1053D0003DB1012D03D0FFDF022DF9D14AE70420B2 +:1053E000C3E70320C1E770B5050004D02B4CA078BB +:1053F000052806D101E0102070BD0820F0F751FC0F +:1054000008B1112070BD2948EFF7BBFBE0702028E0 +:1054100006D00121F0F777FA0020A560A07070BDDA +:10542000032070BD1D4810B5017809B1112010BDD1 +:105430008178052906D0012906D029B10121017002 +:10544000002010BD0F2010BD00F03BF8F8E770B54C +:10545000124C0546A07808B1012809D155B128465B +:10546000FFF73DFE40B1287840B1A078012809D06F +:105470000F2070BD102070BD072070BD2846FFF7BB +:1054800058FE03E000212846FFF772FE0449E07849 +:10549000EFF73CFE00B9FFDF002070BDD001002017 +:1054A0006C1300203D860100FF1FA1073952020046 +:1054B0000A4810B5006900F013F8BDE81040EFF796 +:1054C000E5BA064810B5C078EFF7B7FB00B9FFDFC3 +:1054D0000820F0F7D0FBBDE81040EBE5D00100203C +:1054E0000C490A6848F202139A4302430A60704763 +:1054F000084A116848F2021301EA03009943116057 +:1055000070470246044B10201344FC2B01D8116055 +:1055100000207047C80602400018FEBF7047704761 +:105520007047704740EA010310B59B070FD1042A6A +:105530000DD310C808C9121F9C42F8D020BA19BA5E +:10554000884201D9012010BD4FF0FF3010BD1AB1C3 +:10555000D30703D0521C07E0002010BD10F8013B18 +:1055600011F8014B1B1B07D110F8013B11F8014B3F +:105570001B1B01D1921EF1D1184610BD032A40F227 +:10558000308010F0030C00F0158011F8013BBCF1E5 +:10559000020F624498BF11F801CB00F8013B38BFFD +:1055A00011F8013BA2F1040298BF00F801CB38BF0B +:1055B00000F8013B11F0030300F02580083AC0F029 +:1055C000088051F8043B083A51F804CBA0E80810D1 +:1055D000F5E7121D5CBF51F8043B40F8043BAFF304 +:1055E0000080D20724BF11F8013B11F801CB48BF5E +:1055F00011F8012B24BF00F8013B00F801CB48BF94 +:1056000000F8012B704710B5203AC0F00B80B1E8CC +:105610001850203AA0E81850B1E81850A0E81850E7 +:10562000BFF4F5AF5FEA027C24BFB1E81850A0E8F0 +:10563000185044BF18C918C0BDE810405FEA827C0A +:1056400024BF51F8043B40F8043B08BF7047D20721 +:1056500028BF31F8023B48BF11F8012B28BF20F8C2 +:10566000023B48BF00F8012B704702F0FF0343EAFA +:10567000032242EA024200F002B84FF0000204297D +:10568000C0F0128010F0030C00F01B80CCF1040C71 +:10569000BCF1020F18BF00F8012BA8BF20F8022BA5 +:1056A000A1EB0C0100F00DB85FEAC17C24BF00F84B +:1056B000012B00F8012B48BF00F8012B70474FF079 +:1056C000000200B5134694469646203922BFA0E852 +:1056D0000C50A0E80C50B1F12001BFF4F7AF09075E +:1056E00028BFA0E80C5048BF0CC05DF804EB89004F +:1056F00028BF40F8042B08BF704748BF20F8022B92 +:1057000011F0804F18BF00F8012B704770477047A9 +:1057100070477047FEDF18490978F9B904207146CF +:1057200008421BD10699154A914217DC06990229B5 +:1057300014DB02394878DF2810D10878FE2807D01A +:10574000FF280BD14FF001004FF000020C4B18471F +:1057500041F201000099019A094B1847094B002BAF +:1057600002D01B68DB6818474FF0FF3071464FF0DE +:105770000002034B1847000028ED00E00060020023 +:105780003D4A020004000020174818497047FFF7FF +:10579000FBFFDBF713FD00BD154816490968884279 +:1057A00003D1154A13605B68184700BD20BFFDE7B1 +:1057B0000F4810490968884210D1104B18684FF003 +:1057C000FF318842F2D080F308884FF020218842D0 +:1057D00004DD0B48026803210A4302600948804740 +:1057E00009488047FFDF000080130020801300205D +:1057F00000100000000000200400002000600200F3 +:1058000014090040C52F000099570200042071467A +:10581000084202D0EFF3098101E0EFF308818869C3 +:1058200002380078102813DB20280FDB2C280BDB34 +:105830000A4A12680A4B9A4203D1602804DB094ADB +:105840001047022008607047074A1047074A104770 +:10585000074A12682C3212681047000054000020DA +:10586000BEBAFECA0514000041410200E34B02002B +:10587000040000200D4B0E4908470E4B0C49084709 +:105880000D4B0B4908470D4B094908470C4B08497C +:1058900008470C4B064908470B4B054908470B4B7B +:1058A000034908470A4B024908470000E1BC0000D1 +:1058B0005DC00000552D0000CF2B00005D2B0000C7 +:1058C000F72D0000211400001B2900004D2F0000BF +:1058D000C911000000210160818070470021016032 +:1058E0004160017270470A6802600B79037170476A +:1058F000959600003F980000A1990000059A0000CD +:105900003F9A0000739A0000AD9A0000DD9A0000F3 +:10591000579B00008D970000C5990000A71200005A +:10592000C14300000D44000073440000FF44000028 +:1059300023460000E546000017470000EF4700003F +:1059400087480000DB480000C1490000E149000031 +:10595000C3160000E7160000171600006B160000C3 +:1059600019170000AD17000047600000F761000044 +:10597000BD650000D56600005F670000DD670000C0 +:105980004168000061690000316A00009D6A000002 +:10599000034A0000094A0000134A00007B4A000045 +:1059A000A74A0000634C00008D4C0000C54C00006D +:1059B0002F4D0000194E00002F4E00003144000012 +:1059C000A7120000A7120000A7120000A7120000F3 +:1059D000A7120000A7120000A7120000A3250000D4 +:1059E000292600004526000061260000EF27000060 +:1059F0008B26000095260000D7260000F92600001F +:105A0000D527000017280000A7120000A7120000E9 +:105A1000CB830000EB830000F58300002F8400009F +:105A20005D8400004D850000DB850000EF850000EF +:105A30003D86000053870000F9880000218A00009D +:105A40004F730000398A0000A7120000A71200005F +:105A5000D9B5000043B7000097B7000003B80000B5 +:105A6000B3B80000010000000000000010011001A8 +:105A70003A0200001A02000405060000FFFFFFFFC3 +:105A80000000FFFFCDAD0000233D000049210000D4 +:105A900099730000118F000000000000D5910000F4 +:105AA00099910000C3910000AB910000000002003A +:105AB00000000000000200000000000000010000E3 +:105AC000000000007781000057810000C5810000C0 +:105AD00025250000E72400000725000037A9000065 +:105AE00063A900006BAB000041590000E581000094 +:105AF0000000000015820000732500000000000077 +:105B000000000000000000004DAA0000000000009E +:105B1000D55900000300000001555555D6BE898EA9 +:105B200000006306630C631200000703AB054F0817 +:105B3000000053044308330C00000000900A0000EA +:105B4000900A0000C3560000C35600009D430000A9 +:105B500079AC00001B7600005B2000001D380200BD +:105B6000E1A401000157000001570000BF430000FD +:105B7000DBAC00009F760000CD2000004938020019 +:105B8000F5A4010070017001400038005C002400A1 +:105B90005001080200000300656C74620000000000 +:105BA000000000000000000000000000870000006E +:105BB000000000000000000000000000BE83605AEA +:105BC000DB0B376038A5F5AA9183886C01000000D3 +:105BD000BB31010081400100000000010206030406 +:105BE00005000000070000000000000006000000A3 +:105BF0000A0000003200000073000000B400000042 +:105C0000EB8F01006F1F020017F90000D9B70100E8 +:105C1000F3F70100D9B70100B5FA000097B9010008 +:105C2000E9F3010097B90100F1F6000025B9010080 +:105C300011F7010025B9010013F90000EDB70100CB +:105C4000D5EF0100EDB7010067FF000019BC0100AE +:105C5000A7F8010019BC0100F401FA0096006400E5 +:105C60004B0032001E0014000A0005000200010073 +:105C70000049000000000000AAAED7AB154120107B +:105C80000C0802170D010102090901010602091899 +:105C9000180301010909030305555555252627D683 +:105CA000BE898E00F401FA00960064004B003200B9 +:105CB0001E0014000A000500020001002549000032 +:105CC000000000009D480200B5480200CD480200D7 +:105CD000E5480200154902003D49020067490200FB +:105CE0009B490200534502009B4402008D41020083 +:105CF00003550200395D0100495D0100755D010039 +:105D0000475E01004F5E0100615E0100A746020090 +:105D1000C1460200954602009F460200CD460200A1 +:105D20000347020023470200414702004F47020099 +:105D30005D4702006D470200854702009D47020053 +:105D4000B3470200C94702000000000087BA000004 +:105D5000DDBA0000F3BA000061500200B941020050 +:105D60007F420200E7530200255402004F54020014 +:105D7000195C010079600100DF470200054802005C +:105D8000294802004F4802001C0500402005004041 +:105D900000100200B45D020008000020E4010000D1 +:105DA00044110000E85D0200EC01002094110000A5 +:105DB000A0110000011413F8130240200B20040668 +:105DC000441A0102228C2720FB349B5F8012800240 +:105DD0001E101B430B5419042A8608019F0916CB79 +:085DE000327F0B6CF410C000CF +:02000004000FEB +:1040000000000420CDB20F00F5B20F00F7B20F0090 +:10401000F9B20F00FBB20F00FDB20F00000000006C +:10402000000000000000000000000000C1450F007B +:1040300001B30F000000000003B30F00214D0F007B +:10404000354E0F0007B30F0007B30F0007B30F0083 +:1040500007B30F0007B30F0007B30F0007B30F003C +:1040600007B30F0007B30F0007B30F0007B30F002C +:1040700007B30F0007B30F0007B30F0007B30F001C +:1040800007B30F0085720F0007B30F0007B30F00CF +:1040900041730F0007B30F00814B0F0007B30F00F0 +:1040A00007B30F0007B30F0007B30F0007B30F00EC +:1040B00007B30F0007B30F0000000000000000006E +:1040C00007B30F0007B30F0007B30F0007B30F00CC +:1040D00007B30F0007B30F0007B30F0005850F00EC +:1040E00007B30F0007B30F0007B30F000000000075 +:1040F0000000000007B30F000000000007B30F002E +:1041000000000000000000000000000000000000AF +:10411000000000000000000000000000000000009F +:10412000000000000000000000000000000000008F +:10413000000000000000000000000000000000007F +:10414000000000000000000000000000000000006F +:10415000000000000000000000000000000000005F +:10416000000000000000000000000000000000004F +:10417000000000000000000000000000000000003F +:10418000000000000000000000000000000000002F +:10419000000000000000000000000000000000001F +:1041A000000000000000000000000000000000000F +:1041B00000000000000000000000000000000000FF +:1041C00000000000000000000000000000000000EF +:1041D00000000000000000000000000000000000DF +:1041E00000000000000000000000000000000000CF +:1041F00000000000000000000000000000000000BF +:104200000348044B834202D0034B03B11847704765 +:10421000C8860020C8860020000000000548064926 +:104220000B1AD90F01EBA301491002D0034B03B1C4 +:1042300018477047C8860020C8860020000000008C +:1042400010B5064C237843B9FFF7DAFF044B13B1DE +:104250000448AFF300800123237010BDC8860020FE +:10426000000000005CBD0F0008B5044B1BB1044901 +:104270000448AFF30080BDE80840CFE7000000002D +:10428000CC8600205CBD0F00A3F5803A704700BFCC +:10429000154B002B08BF114B9D46FFF7F5FF002182 +:1042A0008B460F461148124A121A00F075F80C4B53 +:1042B000002B00D098470B4B002B00D098470020D4 +:1042C000002104000D000B4800F016F800F040F843 +:1042D0002000290000F074FA00F014F80000080033 +:1042E000000000000000000000000420C88600203C +:1042F000A4CE002025430F00002301461A4618468D +:1043000000F09CB808B50021044600F0CBF8044B3F +:104310001868C36B03B19847204600F029F900BF25 +:1043200058BB0F0038B5084B084D5B1B9C1007D0DD +:10433000043B1D44013C55F804399847002CF9D141 +:10434000BDE8384007F002BCC8860020C4860020C3 +:1043500070B50D4E0D4D761BB61006D0002455F8E5 +:10436000043B01349847A642F9D1094E094D761B0A +:1043700007F0E6FBB61006D0002455F8043B0134E4 +:104380009847A642F9D170BDBC860020BC860020AB +:10439000C4860020BC860020830730B548D0541E58 +:1043A000002A3FD0CAB2034601E0013C3AD303F8E9 +:1043B000012B9D07F9D1032C2DD9CDB245EA052556 +:1043C0000F2C45EA054536D9A4F1100222F00F0C56 +:1043D00003F1200EE6444FEA121C03F1100242E9F9 +:1043E000045542E9025510327245F8D10CF1010230 +:1043F00014F00C0F03EB021204F00F0C13D0ACF10D +:10440000040323F003030433134442F8045B934290 +:10441000FBD10CF003042CB1CAB21C4403F8012BED +:104420009C42FBD130BD64461346002CF4D1F9E721 +:1044300003461446BFE71A46A446E0E770B4184C9A +:104440002568D5F848411CB365681F2D25DC38B9AF +:10445000AB1C0135656044F82310002070BC704728 +:1044600004EB850C0228CCF88820D4F888614FF042 +:10447000010202FA05F246EA0206C4F88861CCF8A5 +:104480000831E5D1D4F88C311343C4F88C31DFE71F +:1044900005F5A674C5F84841D6E74FF0FF30DDE7D3 +:1044A00058BB0F002DE9F84F2B4B1F68D7F8486118 +:1044B0002DED028BC6B108EE100A8B464FF00108B5 +:1044C0004FF000097468651E0ED4013406EB8404B5 +:1044D000BBF1000F0CD0D4F800315B4508D0013D92 +:1044E0006B1CA4F10404F3D1BDEC028BBDE8F88F82 +:1044F00073682268013BAB420CBF7560C4F8009042 +:10450000002AECD0D6F88801D6F804A008FA05F104 +:1045100001420BD190477268524513D1D7F8483108 +:10452000B342DCD01E46002ECCD1DDE7D6F88C019C +:1045300001420CD1D4F8801018EE100A904772682E +:104540005245EBD0D7F84861002EBBD1CCE7D4F868 +:1045500080009047DFE700BF58BB0F00024B13B14C +:104560000248FFF7C9BE70470000000025430F0056 +:10457000FEE700BF38B50C46E8B90968C9B10F4D70 +:10458000A9420BD06B1A3B2B11D93C22284606F0CE +:10459000EDFE03E0CA5CEA54013BFBD2074800226F +:1045A0003C2103F0D3F90023A887236038BD3D23C5 +:1045B000F2E70E23F9E70123F7E700BF807F002031 +:1045C000074A6FF002039E4502D1EFF3098101E033 +:1045D000EFF308818869A0F102000078104700BF5E +:1045E00075450F0038B50446A8B10D4D00223C2199 +:1045F000284603F0ABF9AB8F83420ED12A4605F172 +:104600003C0152F8040B44F8040B8A42F9D10133FF +:10461000AB87002038BD0E20FCE70B20FAE700BF77 +:10462000807F00200B2970B50446154608462FD917 +:104630002389053304EB43012044431ADAB2012AEB +:1046400026D9814224D8134806F090FE2388522BA5 +:1046500006D1AB0711D062884CF668639A420CD041 +:104660000F2014E034F8022B824204D0AE89964227 +:1046700003F1010308D1002009E0218900230A3455 +:104680004FF6FE704FF440559942EBD80B2070BDA9 +:104690000920FCE7E486002008B5002203F056F963 +:1046A000034B1B88834214BF0B20002008BD00BFB2 +:1046B000E486002038B50C4C21684B1C054612D00E +:1046C0000A484FF4805206F01FFE48B115B1206829 +:1046D00000F00CFC054920684FF4806200F026FCD5 +:1046E0004FF0FF33236038BD30840020F086002077 +:1046F0002DE9F041DFF84480044624F47F65184634 +:10470000D8F8003025F00F05AB420E46174609D009 +:10471000FFF7D0FF0848C8F800504FF480522946F0 +:1047200006F024FE0448C4F30B043A463146204404 +:10473000BDE8F04106F01ABEF0860020308400206B +:10474000BFF34F8F0549064BCA6802F4E06213437A +:10475000CB60BFF34F8F00BFFDE700BF00ED00E06F +:104760000400FA054BDF704710DF704711DF704718 +:1047700013DF704718DF704760DF704769DF7047ED +:1047800061DF70471FB50023CDE90133039368460D +:1047900002230093FFF7EEFF05B05DF804FB08B5B8 +:1047A0004FF0E023D3F8F03DDB0700D500BEFFF764 +:1047B000C7FF0000014B1878704700BFF19600203A +:1047C0002DE9FF484C4B40F60212C3F8402500F09B +:1047D00005FA00F021FE002000F0AAFA00F09CFE8D +:1047E00048B1052000F0A4FA00F0A8FE00F0CCFECD +:1047F000062000F09DFA4FF08043DFF81081D3F8D7 +:104800001C55B12D0CBF0123002388F800304AD07D +:10481000A5F1A8014C424C41384EDFF8F49004F069 +:10482000010333704FF08043D3F8007407F00107A1 +:10483000002C3BD14E2D38D0572D36D0304B1B6835 +:104840001A68304B9A4200D17FBB6D2D2ED01220BA +:1048500000F0B0F9044633789BBB122000F0AAF9AF +:1048600010B1122000F0A6F900F00100307000F045 +:1048700005FDDFF8A0B0824630B12CB9204B1B6893 +:104880001A68214B9A4257D04FF440535A684A4510 +:104890000ABF9B684FF4905303F500731B685B4598 +:1048A0003AD1012448E00124B6E701244FF08043C7 +:1048B00000226D2DC3F81C2500F0E480002CCAD125 +:1048C000C5E70120D0E7544636E03C4634E04FF4DB +:1048D00080030B6071E0022000F02AFAA5F14E037C +:1048E0005842584103F012FEAEE000221146C1E0EA +:1048F00003F05CFEC6E000BF00A00040F19600207F +:1049000034840020D51A5A007E67E54EF2960020C6 +:10491000DBE5B1517CB0EE87002CC2D1BAF1000FBB +:10492000D1D0002FD1D0654B654A1B6865481A600D +:10493000654B43F0010398474FF440535A684A458A +:1049400008BF9B685D4A0CBF03F500734FF490539A +:1049500012681B685B450CBF5C4B002313601CB9DD +:10496000BAF1000F40F08E803378002BB3D00820CE +:1049700000F0DEF998F800300BB9FFF703FF0123D0 +:104980004FF4742088F80030FFF7F2FE504B514985 +:104990001868019001A8FFF7E7FE4F4991F8163318 +:1049A0005A09EC231341DA0707D54C4B9A68002AC1 +:1049B0008DD01A6842F480021A600C22484B029390 +:1049C00000210DEB0200FFF7E7FC40F20113029A11 +:1049D000039303A94020FFF7D1FE0C2200210DEB29 +:1049E0000200FFF7D9FC9DF80C30029A43F0010356 +:1049F00003A9A0208DF80C30FFF7C0FE0C22002187 +:104A00000DEB0200FFF7C8FC01241723029AADF852 +:104A10000E3003A923208DF80C40FFF7AFFE0C22C7 +:104A200000210DEB0200FFF7B7FC0623029A8DF878 +:104A30000C4003A920208DF80E40ADF81030FFF790 +:104A40009DFE02A8FFF798FE4FF4405330785A6855 +:104A50004A450ABF9B684FF4905303F500731B68E7 +:104A60005B4504D0572D02D04E2D7FF43EAF01227E +:104A700040F6B83100F0E8FC3378002B3FF438AF53 +:104A8000FFF774FE00F0EEF800F0F8FBA0B100F0C4 +:104A900043FD88B94FF440535B684B4506D198F805 +:104AA00000300BB9FFF76EFEFFF760FE034B1B688B +:104AB00000221A6000F004FDFFF742FE348400205B +:104AC000D51A5A000048E80160BB0F007E67E54E2A +:104AD0005CBB0F009F470F0000E100E0409D0020FD +:104AE0000080002010B58EB03423ADF802300DF1F7 +:104AF0000201002301A8ADF80430FFF741FE04468F +:104B000040B9BDF80430102B07D0112B0CD001A8F0 +:104B100000F0CCFF20460EB010BD054B01221A70EC +:104B2000072000F005F9F2E7014B18700820F8E7BC +:104B3000F096002013B5002301A80193FFF712FEA1 +:104B4000044660B9019802F053FA019B0A2B09D080 +:104B5000092B09D00B2B02D1012004F09BFB20462E +:104B600002B010BD2046F8E70220F6E708B5FFF7CF +:104B7000B9FF0528FBD1FFF7DDFF0528F7D108BDF8 +:104B80000021024A084602F003BE00BF6D4B0F0031 +:104B90001F2884BF00F01F00044B054A98BF4FF048 +:104BA000A04300F5E07043F8202070470003005058 +:104BB0000C0003001F288ABF064B4FF0A04300F0F3 +:104BC0001F00D3F8103523FA00F0C04300F00100B5 +:104BD000704700BF000300507047000008B54FF059 +:104BE000804301220021DA601220C3F818159A6070 +:104BF000FFF7CEFF1220FFF7CBFF154B4FF4C85045 +:104C000043F001039847FFF7E7FF124A1E210820EF +:104C100002F09AFD08B102F051FE02F0FBFC0E49D1 +:104C20000E4BE02081F823001B684FF47A72B3FB2F +:104C3000F2F3013BB3F1807F08D24FF0E0225361E1 +:104C4000002381F8230093610723136108BD00BF8F +:104C500070BB0F00F496002000ED00E038840020C7 +:104C6000704700004FF0E0224FF40031002310B5F0 +:104C70001361C2F8801102F1C04202F540524FF4B4 +:104C80008031C2F84813C2F80813012151609160C5 +:104C90004FF080420A4CD16002201F2B1A46C6BF3B +:104CA00003F01F0221464FF0A04102F5E0720133EC +:104CB000302B41F82200F0D1FFF7D2FF10BD00BF2A +:104CC00000030050074B23F81010074B002282B05E +:104CD000C3F81021D3F810210192019A01229A60A1 +:104CE00002B07047E898002000C001400A4A0B4B10 +:104CF00011681B68B1FBF3F203FB1211B1EB530F08 +:104D00004FEA530288BF591A4F2359430020B1FB81 +:104D1000F2F189B2FFF7D6BFE4980020F0980020A6 +:104D2000024A136801331360FFF7E0BFE4980020E4 +:104D30001B490A68082823D8DFE800F0130513226E +:104D4000221A1E242A00174B40F6B83018604FF480 +:104D50007F4303F0103323F080539A4218BF0B6057 +:104D60007047104B4FF4967018604FF47F03F0E7D4 +:104D70000C4B64221A6070470A4B40F6B83018603A +:104D80001346E6E7074B40F6B8301860FF23E0E72C +:104D9000044B4FF4967018604FF0FF13D9E700BF33 +:104DA000F4980020F098002000F1804382B01A6847 +:104DB000002A14BF0120002004D000221A601B68C2 +:104DC0000193019B02B070470F4A1378D3B903785F +:104DD0004FF08041C3F34003C1F88035037803F0FE +:104DE0000103C1F87835094B1968C90706D4E021D9 +:104DF00083F800130121C3F88011196001230448CE +:104E000013707047034870470499002000E100E0E8 +:104E10000000AD0B0C00AD0B014B02681A6070472F +:104E2000009900204FF080434FF46072C3F80423D0 +:104E30007047000010B54FF08043D3F80443620779 +:104E400007D54FF48470FFF7AFFF10B11E4B1B68FE +:104E50009847A30608D54FF48A70FFF7A5FF18B14D +:104E60001A4B00201B689847600608D54FF48C70D9 +:104E7000FFF79AFF18B1154B01201B6898472106D0 +:104E800008D54FF48E70FFF78FFF18B1104B00203C +:104E90001B689847E20508D54FF49070FFF784FF30 +:104EA00018B10B4B01201B689847A3050AD54FF496 +:104EB0009270FFF779FF28B1054BBDE810401B68E1 +:104EC0000220184710BD00BFF8980020FC98002071 +:104ED00000990020044AD2F80034DB07FBD50160BA +:104EE000BFF35F8F704700BF00E001404FF0805379 +:104EF0001A69B0FBF2F302FB130373B9084B0222E9 +:104F0000C3F80425C3F80805D3F80024D207FBD55D +:104F100000220448C3F8042570470348704700BFC7 +:104F200000E001400000AD0B0A00AD0BF8B50B4BE3 +:104F30001546012206460F46C3F804250024A54263 +:104F400004D1064B0022C3F80425F8BD57F82410FD +:104F500006EB8400FFF7BEFF0134F0E700E00140FC +:104F6000BFF34F8F0549064BCA6802F4E062134352 +:104F7000CB60BFF34F8F00BFFDE700BF00ED00E047 +:104F80000400FA054FF08053D3F83021082A06D1E7 +:104F9000D3F83431032B02D8024AD05C704700208A +:104FA000704700BF76BB0F0008B54FF08053D3F8B1 +:104FB0003021082A4ED14FF080420021C2F80C1156 +:104FC000C2F81011C2F8381502F54042D3F80414A3 +:104FD000C2F82015D3F80814C2F82415D3F80C141D +:104FE000C2F82815D3F81014C2F82C15D3F81414ED +:104FF000C2F83015D3F81814C2F83415D3F81C14BD +:10500000C2F84015D3F82014C2F84415D3F824147C +:10501000C2F84815D3F82814C2F84C15D3F82C144C +:10502000C2F85015D3F83014C2F85415D3F834141C +:10503000C2F86015D3F83814C2F86415D3F83C14DC +:10504000C2F86815D3F84014C2F86C15D3F844348C +:10505000C2F87035FFF796FF18B1494B494AC3F8BB +:105060008C26FFF78FFF18B1474BFB22C3F818259A +:10507000FFF788FF70B14FF080414FF08053D1F8B7 +:10508000E42ED3F8583222F00F0203F00F0313433B +:10509000C1F8E43EFFF776FF20B13C4B4FF40072BD +:1050A000C3F840264FF08053D3F83031082B09D194 +:1050B0004FF08043D3F80024D10744BF6FF00102C2 +:1050C000C3F80024324AD2F8883043F47003C2F89F +:1050D0008830BFF34F8FBFF36F8F4FF01023D3F89B +:1050E0000C22D2071DD52B4B0122C3F80425D3F87F +:1050F0000024002AFBD04FF01022D2F80C3223F00B +:105100000103C2F80C32234BD3F80024002AFBD051 +:105110000022C3F80425D3F80024002AFBD0FFF7AF +:105120001FFFD3F80022002A03DBD3F80432002B40 +:1051300022DA184B0122C3F80425D3F80024002AF0 +:10514000FBD04FF010221221C2F80012D3F8002435 +:10515000002AFBD04FF010231222C3F804220D4B7B +:10516000D3F80024002AFBD00022C3F80425D3F88A +:105170000024002AFBD0D2E7074B084A1A6008BD7A +:10518000005000404881030000F0004000900240C1 +:1051900000ED00E000E00140388400200090D003E2 +:1051A00013DF704718DF7047064B1878012803D1CA +:1051B000012904BF0221197012B1104602F07EBB12 +:1051C000704700BF3599002008B5FFF7F3FA88B1A2 +:1051D00011481C2101F098FF08B102F06FFB0F4944 +:1051E0000D4800231C2201F07FFF98B1BDE8084064 +:1051F00002F064BB4FF47F20FFF778FE07220749D7 +:105200004FF47F20FFF792FE054B1A78012A04BF66 +:1052100002221A7008BD00BF2C9900203899002086 +:105220003599002070B5124C124D134ED4F800344D +:105230007BB1C4F80056C4F80456C4F80856C4F844 +:105240000C56C4F81056C4F81456C4F81856C4F8CE +:105250001C5602F005FB05F0F4FF20B104F0DAFD66 +:10526000002005F003FA3378023B022BDED870BD34 +:10527000000001403546526E3599002013B54FF4B9 +:105280004053124A596891420CBF9C684FF48054B5 +:1052900001A800F0A7F92368013302D16368013344 +:1052A00011D0019B1A88012A0DD1588820B1996824 +:1052B0000022204602F04AFB019B5B881B1A5842E1 +:1052C000584102B010BD0020FBE700BFDBE5B15143 +:1052D00084B02DE9F34108AC84E80F009DF820402C +:1052E000BDF8228001A80F4616461D4600F07AF947 +:1052F00054B9384B0122FF21A3F802809D601A8027 +:105300009980354B1A7012E0012C17D1314BBA1924 +:105310002A449A60A5221A80FF229A800C9AA3F848 +:105320000280C3E903765D619A612B4B1C70FFF725 +:105330004BFF02B0BDE8F04104B07047032C0FD121 +:10534000019A244B11881980518892689A60C3E9A8 +:105350000376AA2259809A805D611F4B0122D1E712 +:10536000022C15D1019A1B4B1188A5290AD10022C4 +:105370009A60FF221A60FF229A800022C3E903226A +:105380005A61EAE719805188926859809A60F2E779 +:10539000052C0ED1FFF70EFA40B100F097FD08B1D1 +:1053A00002F08CFA0C4B03221A70C2E700F010FADC +:1053B000F5E7042C08D1074B00229A60FF221A60FF +:1053C000019A92889A80B2E7062CB2D1024B04224D +:1053D000EAE700BF389900203599002000B50C4B52 +:1053E0001B7889B063B90B4B1B786BB905238DF81B +:1053F0000C30079B009303AB0FCBFFF769FF03E073 +:1054000004F000FB0028EED009B05DF804FB00BFFB +:1054100034990020289900201FB50023CDE90233DC +:10542000074B019301F030FE30B906494FF47F235A +:1054300001A84B6001F046FE05B05DF804FB00BF1B +:10544000A9510F002C99002070B505460E460AB1EF +:1054500080F00102154B02F001021A7000F0B0FF5B +:10546000044628B935B100F08BFC0446FFF7DAFE9C +:10547000204670BDBEB10E4B0E4A0F481D70294626 +:1054800002F0F8F84FF400444FF4FA7029464FF454 +:105490007A720023E6FB040106F0D0F92A460146A1 +:1054A000064802F0F9F800F06FF9DEE734990020C1 +:1054B00028990020DD530F007CBB0F0008990020C5 +:1054C0001FB5134B4FF0FF32C3F88020C3F8802183 +:1054D0004FF440530F4A596891420DD19C682046C1 +:1054E000FFF75EFE10B14FF000531C60204604B081 +:1054F000BDE8104000F09AB80023CDE902334FF424 +:10550000805406236846CDE90034FFF74BFEE9E7F7 +:1055100000E100E0DBE5B15107B501A800F062F859 +:10552000019B1A88A52A07D09888A0F1AA0358429F +:10553000584103B05DF804FB0120FAE710B501F013 +:105540005DF9A8B10E4B0F4843F00103984701F0F5 +:10555000BFF808B102F0B2F901F050F908B102F059 +:10556000ADF901F001F9044638B102F0A7F904E001 +:1055700001F01EF904460028E4D1204610BD00BF0A +:1055800080BB0F0000A8610000B589B003AB1422F6 +:1055900000211846FEF700FF02228DF80C200022A1 +:1055A00000920FC8FFF794FEFFF73CFE002009B001 +:1055B0005DF804FB13B5044601A800F013F8019B45 +:1055C0001A8822805A8862809A68A2609A88A2808B +:1055D000DA68E2601A6922615A6962619B69A361B3 +:1055E00002B010BD014B0360704700BF00F00F0018 +:1055F000F0B50346186880F308885868FF2464B241 +:10560000EFF30585002D01D1A64600472546064645 +:1056100021273FBAF0B40024002500260027F0B46B +:10562000F92040B2004700BFF0BD00BFFFF7E0BF68 +:1056300073B500230DF1020101A8ADF8023001930A +:1056400002F0C4FDF8B9019C25785DB3174B93F8BF +:105650003020032A28D00C2606FB00F29958E9B91D +:1056600098189D5093F830200132D2B283F8302040 +:10567000BDF802300E4A9B08013B0434436084604D +:10568000084602F085F8019B33B128B1184602F0B4 +:10569000B9FD08B102F012F902B070BD0130042862 +:1056A000DAD1F0E70720EEE70420ECE75499002078 +:1056B000F9560F00084609B102F000B97047000022 +:1056C00010B50C220B4B504319181A5882B193F89D +:1056D00030208C68013AD2B283F8302000221A5070 +:1056E000C1E90122201F02F08DFD08B102F0E6F8A9 +:1056F000002010BD54990020214B70B50122214E8D +:105700001A7096F8303003B970BD1E4C002523681E +:1057100083B1013B042B07D8DFE803F01C0612031A +:105720002800204600F0DEFEE8B2FFF7C9FF08B10E +:1057300002F0C4F80135042D04F10C04E7D1E0E7D0 +:10574000A3686360204600F073FE0028ECD002F0EE +:10575000B5F8E9E7204600F053FF00F02FFF08B14D +:1057600002F0ACF80520FFF7E3FADDE700F074FF84 +:1057700000F092FFBDE870400620FFF7D9BA00BFE5 +:10578000289900205499002008B50E4B002283F878 +:10579000302004210139C3E901221A6003F10C030E +:1057A000F8D1094800F03EFE02F0A8FC08B102F072 +:1057B00085F8064802F08EFC08B102F07FF8002060 +:1057C00008BD00BF54990020B5560F0031560F0098 +:1057D00008B50020FFF774FF0120FFF771FF0220DA +:1057E000FFF76EFF0320FFF76BFFBDE8084002F0F4 +:1057F000CDBC006870476CDF70476DDF70476EDFAF +:1058000070476FDF704772DF704773DF704774DF78 +:10581000704776DF704777DF70477ADF70477CDF4D +:1058200070477FDF704786DF70478FDF704790DFFC +:105830007047AFDF7047B0DF7047B1DF7047B2DF4E +:105840007047B5DF704764DF704766DF70470C282C +:1058500013D8DFE800F01412121212120912071204 +:10586000120D0B0002207047032070470420704780 +:10587000042914BF06200520704706207047012028 +:10588000704702F01BB810B5044608460321FFF725 +:10589000DEFF0246204601F075F918B1BDE8104060 +:1058A00002F00CB810BD00000346032B10B50846EB +:1058B000144620D0042B23D169B1124B18884FF61F +:1058C000FF7398421CD01321FFF7A3FFC0B1BDE8BE +:1058D000104001F0F3BF104602F094F808B101F057 +:1058E000EDFF094B1B689C420AD1012203210748A6 +:1058F00001F048F9EAE70121FFF7A9FF0246F6E7C0 +:1059000010BD00BF3E840020489A0020F899002076 +:10591000F8B50A4DAB889E181D2E14460DDC2F6875 +:10592000FE1802F1010C07F803C07070B01C05F0FE +:105930001DFDAB8802331A19AA80F8BDA899002072 +:10594000F0B54E4E317895B0002940F092804C4C25 +:10595000019110222046FEF71FFD4A4B019923605A +:1059600018220EA8FEF718FD01238DF838302823E1 +:105970001093454B1B78002B7DD0444B04AC03F1B6 +:10598000100518685968224603C20833AB42144612 +:10599000F7D13F4C10220DEB0201E01D05F0B4FCE5 +:1059A000002868D03B4801210460FFF728FF08B1B8 +:1059B00001F084FF384B08AA03F1100C1746186851 +:1059C0005968154603C5083363452A46F7D1206850 +:1059D0000C903248A288A379ADF8342007600122E8 +:1059E00000218DF83630FFF70CFF08B101F066FF9B +:1059F00003238DF84C3004238DF80E3041F23053E0 +:105A0000ADF81030264B08AA9B798DF812300DF1B5 +:105A10000F0104A8FFF717FF012210460DF10E0138 +:105A2000FFF776FF1F4805F04BFE1E49C2B2092062 +:105A3000FFF76EFF102208A90620FFF769FF104943 +:105A400019480EAAFFF7DFFE08B101F037FF164C28 +:105A5000042221780120FFF7DEFE08B101F02EFFBD +:105A600020780121FFF7D1FE08B101F027FF0123C3 +:105A7000337015B0F0BD0623BEE700BF2C9A00209E +:105A8000A899002088990020F4990020A9BB0F0054 +:105A9000B8990020449A0020BF990020289A00203D +:105AA000F899002086BB0F003C840020F0B5044626 +:105AB0000146B1B0A84801F099F82388262B3BD8BD +:105AC0000F2B04D8012B00F0CC8031B0F0BD103B7F +:105AD000162BFAD801A252F823F000BF655B0F0025 +:105AE000735B0F00CB5A0F00A55B0F009F5C0F008C +:105AF000CB5A0F00CB5A0F00CB5A0F00CB5A0F00D6 +:105B0000CB5A0F00BB5C0F00CB5A0F00CB5A0F00D3 +:105B1000CB5A0F00CB5A0F00CB5A0F00CB5A0F00B5 +:105B2000315D0F00CB5A0F00235D0F00CB5A0F00E1 +:105B3000CB5A0F00255C0F00513B9AB2052AC4D8FE +:105B4000052BC2D801A252F823F000BF6F5C0F00F2 +:105B5000BB5C0F00CB5A0F00CB5A0F00475D0F0004 +:105B60000B5C0F007D4BA2881A807D4B00221A70BF +:105B7000ABE78023794CADF824307A4B2088322271 +:105B80001A6010A9012309AAFFF759FE08B101F014 +:105B900095FE754B1B780BB9FFF7D2FE4FF6FF73DE +:105BA000238092E7714B03AC9A79186899888DF835 +:105BB00022200790DA1DADF8201017332646106812 +:105BC0005168254603C508329A422C46F7D1684BE6 +:105BD00009AA03F11807154618685968144603C442 +:105BE0000833BB422246F7D1186820605B48614AFF +:105BF000008810AB8521CDE91456FFF712FE00286E +:105C00003FF463AF01F05AFE5FE7A379002B7FF406 +:105C10005CAF524B13211888FFF7FBFD00283FF4BF +:105C200054AF79E0237A012B7FF44FAF4C4B002225 +:105C30001A704C4B19680139196069B910AB1422FC +:105C40001846FEF7A9FB05228DF84020149A009211 +:105C50000FC8FFF73DFB38E731B0BDE8F040FFF774 +:105C60006FBE3E4B00211888FFF7EFFDD6E7A37902 +:105C7000002B3FF42AAFA27B043A022A3FF625AF5D +:105C8000022B18BF01238DF840304FF4C173ADF8DB +:105C90004430324B10A91888FFF7CDFDAFE7334AE7 +:105CA000258A508D02F118010023854218BF19463C +:105CB0000732A088FFF7B7FDB0E7284B1C884FF6E6 +:105CC000FF75AC4227D02C4B1B78F3B12B49012335 +:105CD00008222046FFF7B1FDF0B902460146022333 +:105CE0002046FFF7AAFDB8B92A460C212046FFF747 +:105CF000A0FDA0F54053023B012B7FF6E6AE08283D +:105D00003FF4E3AE112889D1DFE61A461946204652 +:105D1000FFF793FD82E7082031B0BDE8F04001F0C5 +:105D2000CDBD0E4B002218881146FFF780FD75E7A8 +:105D300000238DF840308DF84130084B10A91888A9 +:105D4000FFF773FDC1E6E188044B1729188828BFC7 +:105D50001721FFF776FD61E7F89900203E840020C7 +:105D60002C9A0020408400203F9A0020B8990020FF +:105D7000D09900203A9A0020F4990020EC99002054 +:105D800030B5464A464800231370464A95B0137012 +:105D900000F048FB01F0F6FD0446002868D14248B7 +:105DA000FEF720FC002866D1404B01221A70112317 +:105DB0003F488DF8043005F083FC3D4982B201A8CC +:105DC000FFF72DFD08B101F079FD0822002104A89C +:105DD000FEF7E2FA0823ADF810301823ADF81230C0 +:105DE0000023ADF8143004A84FF4C873ADF8163092 +:105DF000FFF713FD08B101F061FD00210C2201A89D +:105E0000FEF7CAFA0823ADF804302A4B02932A4859 +:105E10002A4B039301A900F02FFD08B101F04EFDBC +:105E2000274D4022002104A8FEF7B6FA284605F0C7 +:105E300047FCADF810002846059505F041FC079594 +:105E4000204DADF81800284605F03AFC1123ADF8B6 +:105E5000300004A8ADF84C300D9500F0DBFF1A4B74 +:105E600030221A7007225A7010229A70FFF768FDCC +:105E7000204615B030BD04A8FFF7BFFC08B101F003 +:105E80001DFD9DF8113004A801338DF81130FFF786 +:105E9000B2FC00288BD001F011FD88E73F9A00206A +:105EA000A9580F00399A0020B8990020F4990020D1 +:105EB00086BB0F001D5F0F00F899002083580F006C +:105EC0008DBB0F0098BB0F003A9A002010B50F4B06 +:105ED00001221A700E4B18884FF6FF73984207D0B4 +:105EE0001321FFF796FC08B101F0E8FC002010BD7B +:105EF000084C2378002BF9D0074B1878FFF787FC64 +:105F000008B101F0DBFC00232370EFE73F9A00208B +:105F10003E8400202C9A00203C840020F0B50B78B1 +:105F200089B005460C46092B35D8DFE813F02D0063 +:105F3000360041000A001900260007011001440044 +:105F4000150100F089FB0421FFF781FC0246284679 +:105F500000F018FEF8B109B0BDE8F04001F0AEBCA9 +:105F6000FFF7B4FF08B101F0A9FC00F095FB90B178 +:105F700009B0BDE8F04000F09DBBFFF7A7FF002887 +:105F8000F6D001F09BFCF3E7764B01221A704B68C8 +:105F90001A78754B1A7009B0F0BD724B02261E704C +:105FA0004B681B78012BF6D100F008FB3146CBE79C +:105FB0006C4B0322EEE70520FEF7BAFE694B1E7814 +:105FC000022E37D0032E59D0012EE4D104AB10227B +:105FD00018460021FEF7E0F9634A237A12788DF81B +:105FE0001020002203920C2B4FF00302CDE9012078 +:105FF00008D03146284600F0C5FD0028CBD001F07E +:106000005DFCC8E763681846FFF7F3FB0590181DB1 +:10601000FFF7EFFB069003F10800FFF7EAFB07909C +:1060200001A800F005FA0028B5D03146FFF70FFCB3 +:106030000246DFE7237A13F0030011D0C0F1040217 +:106040001A44D2B219464FF0000C0E46013167686F +:10605000C9B2914207F806C0F7D11B1A0433237264 +:106060000123049363680693237A04A89B0805938D +:1060700000F0C6FA00288ED00221D7E7207A8307E5 +:1060800002D03246314662E7384E0190314601F087 +:106090008FFC014618B12846FFF7F5FB7BE76168E6 +:1060A000019A306805F062F9019801F0E7FC0146B9 +:1060B0000028F0D101A9304601F0F0FC014600288B +:1060C000E9D104230493019B9B08059304A833683A +:1060D000069300F007FA074640B9254A237A11686B +:1060E0000B441360234B32681A6054E709281BD114 +:1060F0001F4B217A1A68114419601F4B1B78002B23 +:106100003FF449AF1D4C2388013B9BB22380002BF9 +:106110007FF441AF284600F0FBFC08B101F0CEFB54 +:10612000174B1B88238036E7306801F06BFC014673 +:1061300010B12846FFF7A7FB3946ACE70E4B01220A +:106140001A700F4A8B8813800C4A138023E70A4A7F +:10615000002313700A4AF8E7054B196800F09CFC0D +:10616000F8E600BF399A0020409A00204C9A00209F +:10617000309A0020489A0020389A0020369A002051 +:10618000349A002018DF7047012973B514460D4674 +:106190001A4608D0032912D014B3204602B0BDE835 +:1061A000704001F08BBB0F4B1B78052BF4D10E4BCD +:1061B0001B68002BF0D0214604209847ECE7094EDD +:1061C0003378022BE8D1094B01925B689847064B64 +:1061D00035701B68002BDFD0019A21462846ECE77A +:1061E00002B070BD589A0020509A00205C9A00209E +:1061F00030B589B003AC142200212046FEF7CCF85C +:10620000094B1B88ADF80E30084BDB680693002560 +:10621000079B8DF80C50009394E80F00FFF758F897 +:10622000284609B030BD00BF689A0020B49A00200B +:1062300000B589B003238DF80C300A4B1B88ADF8EC +:106240000E30094B5A6804929A68DB680693079BE4 +:106250000093059203AB0FCBFFF73AF8002009B08B +:106260005DF804FB689A0020B49A002000B589B05C +:1062700001238DF80C300F4B0F4A1B88ADF80E3000 +:106280004FF440535968914208BF9A680B4B5968C4 +:10629000049118BF4FF480529968DB68069305910A +:1062A000009203AB0FCBFFF713F8002009B05DF8A5 +:1062B00004FB00BF689A0020DBE5B151B49A0020CE +:1062C00000B589B003AB142200211846FEF764F82C +:1062D00004228DF80C20002200920FC8FEF7F8FF70 +:1062E00009B05DF804FB0000194BF7B5194C1C60B0 +:1062F000194B02221A70FEF75DFA184B48B1196863 +:10630000204600F001FF00B303B0BDE8F04001F00B +:10631000D5BA1D68124F013D2D0B013504464FF4CF +:1063200040567368BB420CBFB0684FF4805000EB1E +:1063300004300134FEF7DAFDA542F2D80023054807 +:1063400000931A460321FFF71FFF03B0F0BD00BF03 +:10635000CC9A0020C49A0020589A00206C9A002001 +:10636000DBE5B15170B50C4686B00321CDE90210D2 +:106370000546960802A8019304940596FFF702FFCC +:10638000E0B1B4F5805F019B11D8012302A8CDE9EB +:106390000235CDE90446FFF7F5FE78B9032302A8DC +:1063A000CDE90235CDE90446FFF7ECFE06E01A46DA +:1063B000E11AE81AFFF7D6FF0028E6D006B070BD54 +:1063C0001FB5114B0193114B114900241C70114B47 +:1063D00001A81C80CDE9024400F074FE0E4B10B100 +:1063E0001C7004B010BD4FF440520C4954688C42EC +:1063F00007490CBF92684FF480524A60084A002156 +:10640000116001221A70ECE789610F00B09A002038 +:10641000C49A0020689A0020589A0020DBE5B15108 +:10642000549A0020014B1860704700BF509A00201A +:1064300038B54368214C0FCB84E80F002278500711 +:1064400001D5910733D16068830730D1A3689D07D8 +:106450002DD1E16811F0030429D1184408441849EA +:10646000B3F5204F086024D84FF4405315495D68B8 +:106470008D420ABF9B684FF46923C3F56A23984293 +:1064800017D8114B1149196011495960D10709D525 +:10649000104A9A60104B1B78012B0CD1FFF724FF98 +:1064A000204638BD92074CBF0C4A0D4AF1E706243E +:1064B000F6E70C24F4E70824F2E700BFB49A0020C2 +:1064C0006C9A0020DBE5B1515C9A0020E9620F0074 +:1064D000C1620F006D620F00589A002031620F00F8 +:1064E000F1610F002DE9F04385B0002853D0816899 +:1064F00011F0030451D12C4B1B78052B4FD12B4E9F +:1065000042683368DFF8AC9003EB82039500D9F85A +:106510000020934207D94FF0FF3333600C2420460C +:1065200005B0BDE8F0830391FEF744F9DFF88880F9 +:10653000039940B13368D8F800002A4600F0D4FD32 +:10654000D8B10446EBE74FF44053194A586837680E +:10655000904208BF9868039118BF4FF48050002301 +:106560002A463844FEF7C4F80399D8F8000000958D +:106570000B4600220121FFF707FE33681D44D9F8BE +:10658000003035609D420CD1FEF714F90028C6D1C9 +:10659000FEF790F8C3E70E24C1E71024BFE70824F4 +:1065A000BDE70924BBE700BF589A0020549A002099 +:1065B000DBE5B1516C9A0020CC9A002070B50B4BF2 +:1065C0001D6885B90A4E3378042B0CD1094C0A4B4F +:1065D00021781A780948FEF725F810B90523337099 +:1065E00070BD2570FCE70820FAE700BF549A002030 +:1065F000589A0020B09A0020B49A0020709A002087 +:10660000F8B5114B1A78032A03D0042A03D00824C2 +:1066100016E004221A700D4B1C68002CF7D10C4DAB +:1066200043682F789E0007EB8303402B0AD88168CC +:1066300008483246384404F099FE2B7833442B70D6 +:106640002046F8BD0924FBE7589A0020549A002000 +:10665000B09A0020709A002010B50B4C2378052BBF +:1066600010D10A4B0A4A1B68116899420AD10623C5 +:106670002370084B1B685868FEF70EF808B907230B +:10668000237010BD0820FCE7589A00206C9A002067 +:10669000549A0020CC9A0020044B1B78072B02D17F +:1066A000034B9B6818470820704700BF589A00208A +:1066B0005C9A002000B589B006238DF80C30079B4A +:1066C000009303AB0FCBFEF703FE09B05DF804FBAC +:1066D000F0B58DB005A8FEF76DFF089C002C3ED0EC +:1066E0000B9E04F58053B3422DD91E4BA6F5805561 +:1066F00003EA55054FF440539B689C420BD8690050 +:106700002B46A4EB450201F5805106EB4500FFF74F +:1067100029FE0DB0F0BD05F58053012701A8CDE994 +:106720000173CDE90337FFF72DFD0028F1D14FF4B8 +:10673000805301A8CDE9023301970497FFF722FDAA +:106740000028E6D1DBE70123CDE90136A4084FF4A8 +:10675000805301A803930494FFF714FDD9E7204662 +:10676000D7E700BF00F0FFFF00B58DB005A8FEF72A +:1067700021FF099880B1089B8BB94FF440530B4A15 +:10678000596891420ED19B6880080022039001A8AD +:10679000CDE90123FFF7F6FC0DB05DF804FB0B9A81 +:1067A0001344F1E74FF48053EEE700BFDBE5B1514E +:1067B00000B58DB005A8FEF7FDFE099898B1089BBD +:1067C000A3B94FF440530C4A5968914211D19B68C8 +:1067D0000393800803214FF47422049001A8CDE9AB +:1067E0000112FFF7CFFC0DB05DF804FB0B9A1344C8 +:1067F000EEE74FF48053EBE7DBE5B15110B58CB019 +:1068000005A8FEF7D7FE0898B8B10B9C00F5805399 +:10681000A34214D94FF440539B6898421BD80F4BA6 +:10682000A4F5805203EA52035900A0EB430201F59C +:10683000805104EB4300FFF795FD0CB010BD8008BC +:1068400003224FF48053049001A8CDE9012303945F +:10685000FFF798FCF1E70E20EFE700BF00F0FFFF25 +:10686000A8DF7047AADF7047ADDF7047AEDF704723 +:10687000B0DF704762DF70472DE9F0470E4694B0F5 +:106880000546002800F00181002900F0FE804B68D9 +:10689000002B00F0FA804FF6FF7303800023ADF861 +:1068A0000A307B4B04AA03F1100C1746186859688C +:1068B000144603C4083363452246F7D141F23053EE +:1068C0000DF10A013846ADF80830FFF7D3FF044652 +:1068D000002840F0D6802A1D02A90120FFF7C0FF42 +:1068E0000446002840F0CD809DF80A30AB71014687 +:1068F0001C220DA8FDF750FD9DF834300E9443F096 +:1069000004038DF8343001AFAB798DF80E30214699 +:1069100041F2325303223846CDE91044CDE9124406 +:10692000ADF80C30FDF738FD9DF806308DF80440C9 +:1069300023F01F0343F00303214614224FF0110AF2 +:1069400008A88DF806308DF805A00DF10C08FDF7AC +:1069500023FD4FF01409A8880A9405F1080308AA3A +:106960000DA90C94CDE90887ADF82C90FFF77AFFBC +:106970000446002840F0858001461C220DA8FDF742 +:106980000BFD9DF834300E9423F0180343F01803E8 +:106990008DF83430AB798DF80E30214641F2315309 +:1069A00003223846CDE91044CDE91244ADF80C304D +:1069B000FDF7F2FC9DF806308DF8044023F01F032C +:1069C00043F0130321464A4608A88DF806308DF897 +:1069D00005A0FDF7E1FC1723ADF82C30A8880A9438 +:1069E00005F1100308AA0DA90C94CDE90887FFF75B +:1069F00039FF0446002844D101461C220DA8FDF7AA +:106A0000CBFC9DF834300E9443F002038DF8343003 +:106A1000AB798DF80E30214641F2345303223846CB +:106A2000CDE91044CDE91244ADF80C30FDF7B4FCCB +:106A30009DF806308DF8054023F01F0343F0030353 +:106A400021464A4608A88DF806308DF804A0FDF7C7 +:106A5000A3FC02230A93ADF82C30A8880C9605F10C +:106A6000200308AA0DA9CDE90887FFF7FBFE04461D +:106A700038B97368AB62B36803B1EB62054B0122AE +:106A80001A70204614B0BDE8F0870E24F9E700BF65 +:106A9000B9BB0F00D09A002070B5054686B070B320 +:106AA00002884FF6FF739A422BD0174B1B7843B3E3 +:106AB000164C1022080AE170207121FA02F0090E2A +:106AC000072301266071A17102A800216370ADF84F +:106AD00006302270A670FDF75FFC2B8AADF80830F7 +:106AE0000023ADF80C3028888DF80A600DF10603FC +:106AF00002A9CDE90434FFF7B9FE06B070BD0E203F +:106B0000FBE70820F9E700BFD09A0020D19A0020C7 +:106B100030B5044687B060B302884FF6FF739A42DF +:106B200029D0164B1B7833B3154D11232B700B0A4C +:106B30006970AB700B0C090EEB70297105230021F5 +:106B4000102202A8ADF80630FDF726FC238AADF826 +:106B5000083001238DF80A300023ADF80C3020886E +:106B60000DF1060302A9CDE90435FFF77FFE07B05A +:106B700030BD0E20FBE70820F9E700BFD09A0020C7 +:106B8000D19A002030B5044687B038B300884FF65C +:106B9000FF73984224D0134B1B780BB3124D102374 +:106BA00069700321ADF80610AA7000211A4602A8E8 +:106BB0002B70FDF7F1FB238AADF8083001238DF827 +:106BC0000A300023ADF80C3020880DF1060302A92D +:106BD000CDE90435FFF74AFE07B030BD0E20FBE7D4 +:106BE0000820F9E7D09A0020D19A002070B50D4610 +:106BF00088B0044650B149B1826A3AB10B88502B33 +:106C000049D005D8102B43D0112B54D008B070BDFB +:106C1000512BFBD18E79022EF8D10A89038A9A4230 +:106C2000F4D18B7B043B022BF0D99DF816308DF804 +:106C3000106043F001038DF816300B8AADF8183060 +:106C40004B8AADF81A30082201F1140301A8002183 +:106C50000793FDF7A1FBA18A2088019601AACDF830 +:106C600008D0FFF701FE48B3E36A03B1984740F24A +:106C7000FD132088ADF8143004A9FFF7F9FD0028B2 +:106C8000C4D0E36A002BC1D008B0BDE870401847FB +:106C90008B882380BAE7C98803899942B6D1082333 +:106CA0008DF81030123535F8023C8DF81830059506 +:106CB00004A99047AAE74FF6FF73EAE7BDF8003052 +:106CC0002088DB07D3D5002604A9ADF81460FFF7B0 +:106CD000CFFD0028D5D1297D4B1E072B3ED8DFE8FC +:106CE00003F004192226282A3B2C6B8A8DF80460B5 +:106CF000012B05D8062201212046FFF743FFBEE7FE +:106D0000012315358DF80C300295A36A01A92046A0 +:106D100098477BE76A8A01239A428DF80430F0D8BD +:106D200006220221E8E702238DF80430EDE7032371 +:106D3000FAE70423F8E70523F6E76B8A022B02D86B +:106D400003220821D8E7B5F81530ADF80830002B3C +:106D50000CBF07230623E7E70923E5E70322CBE778 +:106D6000A8DF7047AADF70472DE9F04180468EB05A +:106D700015461F460E4611B9084600F09FFD15B98D +:106D8000284600F09BFD1C220DEB02000021FDF7C0 +:106D900003FB9DF81C30ADF80480002443F002038F +:106DA0008DF81C3021460123032268468DF80630F9 +:106DB000CDE90A44CDE90C440894FDF7EDFA3B789F +:106DC0008DF800307B788DF801309DF8023023F08B +:106DD0001F0343F002032146142202A88DF802305B +:106DE000FDF7DAFA0A48CDF80CD001AB029302AAFB +:106DF000149B0088ADF8105007A9ADF81240ADF80B +:106E000014500696FFF7AEFF0EB0BDE8F08100BF4C +:106E1000F89A002030B587B041F60A032B4AADF846 +:106E20000C30044603A901208DF80E00FFF798FFEF +:106E30000546D8B92288E2B922894AB1244B009389 +:106E4000E16804F13C0342F62420FFF78DFFD8B936 +:106E5000228C4AB11F4B0093616A04F13C0342F655 +:106E60002620FFF781FF78B9A36B7BB9284607B0CE +:106E700030BD194B0093616804F13C0342F62920B0 +:106E8000FFF772FF0028D7D00546EFE71A788DF894 +:106E900010205A888DF81120120A8DF812209A8835 +:106EA000DB888DF815301B0A8DF813208DF816300D +:106EB000120A0A4B8DF814200093072204F13C03B8 +:106EC00004A942F65020FFF74FFFDDE7F89A0020B3 +:106ED000E89A0020D89A0020E09A0020F09A00203A +:106EE00029DF704728DF7047064B182202FB00306D +:106EF00000230422C0E90423037183608361C3601B +:106F0000704700BF0C9B002023B502460846C968A5 +:106F100043680093044B53F82150436910F80C1B4D +:106F2000A84702B020BD00BFFC9A002038B5194C1C +:106F30002378182202FB03431A795869012A03D0E7 +:106F4000032A1AD00F2038BD134A996915689A6828 +:106F5000DB68A2EB0532B2F5805F184401EB053126 +:106F600000EB053034BF92084FF48062FFF7B8FFA2 +:106F70000028E8D10123A370E5E74FF080531B6997 +:106F80009BB2B0FBF3F0044B1B681844FFF7AAFF59 +:106F9000EEE700BF0C9B0020049C002070B5134D51 +:106FA0006C780A2C1FD02E783444E4B2092C84BFAC +:106FB0000A3CE4B2182606FB0454A261207103C9FE +:106FC000A360049BE360AB7804F1100282E8030045 +:106FD00023B100206B7801336B7070BDFFF7A6FF03 +:106FE0001128F7D1F5E70420F7E700BF0C9B00203C +:106FF00070B5234CA3782BB100260228A67002D0CE +:10700000032833D070BD25781E4A182101FB0541A5 +:10701000136889680133B1EB033F136014D86378B8 +:107020001660013B63706B1CDBB21821092B01FB5E +:10703000054188BFA5F10903002004312370FFF743 +:1070400063FF2846FFF750FF6378002BDAD0A37860 +:10705000002BD7D1FFF76AFF0028D3D01128D1D059 +:107060002178182303FB0141043105E0217818231E +:1070700003FB014104310D20BDE87040FFF744BF20 +:107080000C9B0020049C002008B50A4B00211960CD +:10709000094B1980997008460131FFF725FF0A292D +:1070A000F9D1064B00201860054BC3E90000C3E985 +:1070B000020008BD049C00200C9B0020009C0020C6 +:1070C000FC9A0020064A03461068042807D008608E +:1070D000411C11601A68034B43F8202000207047C0 +:1070E000009C0020FC9A002013B5CC180C43A40788 +:1070F00008D1009313460A4601460120FFF74EFFD0 +:1071000002B010BD1020FBE707B500220B4600922D +:1071100001460320FFF742FF03B05DF804FB0000C7 +:10712000094B5A7899780132D2B2914208BF0022B5 +:10713000197891421FBF02705878182202FB003064 +:1071400014BF043000207047089C0020082910B5A7 +:10715000044602D0002000F0B1FBD4E90030BDE8C5 +:107160001040184773B5054600240DF107000E4680 +:107170008DF8074000F0B0FB0DF10600FFF7D0FFDF +:1071800090B10670094B9DF8062045605A709DF835 +:10719000070000F0C5FB24B9054B4FF48012C3F87B +:1071A0000021204602B070BD0424F0E7089C0020B6 +:1071B00000E100E0204B21491A682F2300BF00BFE7 +:1071C00000BF00BF00BF00BF00BF00BF8A422FD07A +:1071D00000BF00BF00BF00BF00BF00BF00BF00BFB7 +:1071E00000BF00BF00BF00BF00BF00BF00BF00BFA7 +:1071F00000BF00BF00BF00BF00BF00BF00BF00BF97 +:1072000000BF00BF00BF00BF00BF00BF00BF00BF86 +:1072100000BF00BF00BF00BF00BF00BF00BF00BF76 +:1072200000BF00BF00BF00BF00BF00BF00BF00BF66 +:10723000013BC3D1704700BF388400200024F40014 +:107240000C4B0D484FF4003210B5C3F880200124D8 +:107250004FF48033C0F84833C0F808334460FFF778 +:10726000A9FF064B846000201860FFF7A3FF044BC2 +:10727000187010BD00E100E000100140249D0020C6 +:10728000159D00202DE9F3412549264B0025C1F825 +:107290004051C1F84451C1F84851C1F84C51C1F8AE +:1072A0000051C1F804511B68002B34D0D1F80445BB +:1072B0001D49DFF888800968641A24F07F442F464E +:1072C0001968A14212D81A7CDE69641A0D4462B1B1 +:1072D0005A691F7400929B690193424608216846CF +:1072E00000F056FA08B100F0E9FABEB90F4A104BA7 +:1072F00011781B788B4205D10133DBB2022B08BF1A +:107300000023137012780B4B43F822500A4B4FF4B2 +:107310008012C3F8002102B0BDE8F0813346CFE708 +:1073200000100140289D0020249D0020219D002068 +:10733000209D0020189D002000E100E04D710F000D +:107340002DE9F74FA84AA94913780978A84C994222 +:107350003BD00133DBB2022B08BF00231370A549D9 +:107360001278A54B0F6853F822003B1823F07F4397 +:1073700000220B60236815461646944613B942B1A5 +:10738000236006E0196881420DD902B12360091A11 +:10739000196001262368DFF8689200930027BDB9C1 +:1073A000DFF868A268E0401A0E44D968D3F81CE000 +:1073B000C3F800C031B1BA1922F07F42C3E90121FC +:1073C000DD611D4673460122D8E700252E46E1E720 +:1073D0002846ED69874BD0F804C01B68DFF830E21F +:1073E0008168ACEB030222F07F42724500F2AD806F +:1073F0000A4402600122027422680023C0E90133BA +:10740000C361002A40F0AB802060C8E75A1C9AF89C +:107410000210D4F800B0D2B291428AF8002004BF22 +:1074200000228AF80020182202FB03A31A79986828 +:10743000022A77D0032A00F08580012A1CD190F817 +:1074400010C0BCF1000F17D1D96841601A69826081 +:107450005A69C2609B698361684B1B78002B18BF17 +:1074600061464160B6E7904200F09E809046D26946 +:10747000002AF8D1002303749AF800309AF801200A +:107480009A42C3D1236827B9009A9A4201D1002EAB +:1074900042D0002B00F08580D3F80090584C554B1B +:1074A000D4F804651868574F351A3B7825F07F45A6 +:1074B00003359BB94FF48033C4F84433C4F8043324 +:1074C000514B4FF400324FF00108C3F880211A608D +:1074D000C4F80080FFF76EFE87F80080A9452CBF36 +:1074E0004844401920F07F40C4F84005D4F80435E2 +:1074F0009B1B23F07F43801B033320F07F4083429C +:107500000AD9D4F80435C4F84035FFF753FE3E4B92 +:107510004FF40032C3F80021384B00221A7003B038 +:10752000BDE8F08F5A46D846A2E78BF81020DBF86A +:107530001CB00123BBF1000FF7D1002B9CD0C4F885 +:1075400000B099E700231A46F4E7A3EB0C0323F0FD +:107550007F438B4234BFCB1A002303604AE70168A4 +:10756000136899421BD85B1A1360C2614CE7A1EB08 +:107570000C01D3F81CC01A46BCF1000F0AD06346B8 +:10758000D3F800C08C45F2D3ACEB010CC3F800C0BB +:107590009C4613460160C0F81CC0D861FFE6134644 +:1075A000EEE7FFF74DFEB7E7404510D1DBF81C30A2 +:1075B000236063B9DFF83CE001920121C9F80810AB +:1075C000CEF800300D4B1970FFF7F4FD019A1368E7 +:1075D000D269C8F81C2012B111680B4413602368EB +:1075E0005B4518BF012745E7209D0020219D002015 +:1075F000289D0020249D0020189D0020149D00201F +:1076000000100140159D002000E100E0089C0020D2 +:10761000FEFF7F0008B5FFF713FE104B00200B2282 +:1076200018809A700E4B18600E4B18700E4B187025 +:107630000E4B4FF48012E021C3F8802183F814131D +:107640001A6002F18042A2F56F22C2F8080583F8A1 +:107650001113074BD2F804251A6008BD089C0020BE +:10766000289D0020209D0020219D002000E100E0B9 +:10767000249D0020074B9B784BB132B128B10368A1 +:10768000187C20B959745A61704707207047082048 +:10769000704700BF089C00202DE9F743DFF8848085 +:1076A00098F8023005460E461746ABB3A0B304293E +:1076B00030D9436983B3437C0024012B0DF10700CB +:1076C0000CBF8946A1468DF8074000F005F90DF181 +:1076D0000600FFF725FDD8B1012303700F4B45606D +:1076E000D3F80435C0E90497C0E902369DF80630A6 +:1076F00088F801309DF8070000F012F924B9084B12 +:107700004FF48012C3F80021204603B0BDE8F08397 +:107710000424EFE70724F7E70824F5E70010014009 +:1077200000E100E0089C0020064A92783AB130B1AE +:10773000426922B1002202740221FFF713BD082022 +:10774000704700BF089C00204B1C30B5DB0004468E +:1077500012F003009BB20DD1074D2A601A44074B6B +:107760001A60074B1870074B1870074B1C80074BAB +:10777000198030BD0720FCE7349D0020309D00209B +:107780002C9D00203C9D0020389D00203A9D00202B +:107790002DE9F347DFF8C080B8F800308B42064689 +:1077A0000D4617464CD300240DF107008DF8074015 +:1077B00000F092F8244B254A25481B78008892F85F +:1077C00000C084455FFA8CF138BF4C1CDBB238BF77 +:1077D000E4B2A3422ED014781178CBB2884286BF8F +:1077E0000133DBB2002313709DF8070000F098F816 +:1077F0004FF6FF739C4225D0DFF86090D9F8002047 +:107800004FEAC40A42F8347002EBC403AEB1A5B12A +:10781000104BB8F800001B682A4604FB00303146C4 +:1078200003F0A4FDD9F80030534400209D8002B03D +:10783000BDE8F0874FF6FF74D6E700209880F6E7A2 +:107840000920F4E70420F2E73C9D00202C9D002055 +:107850003A9D0020309D0020389D0020349D00205E +:1078600070B5104C104D22782B789A4200D170BD23 +:107870000E480F4A2378126806880E4802EBC301AF +:10788000006852F83320898803FB060090470A49B4 +:1078900022780988D3B2914286BF0133DBB200233C +:1078A0002370E0E73C9D00202C9D0020389D0020A7 +:1078B000349D0020309D00203A9D00201FB50021FE +:1078C000CDE9021001AA44F20100ADF80410FCF762 +:1078D00066FF05B05DF804FB70B5EFF3108672B675 +:1078E0000C4A946801239CB993600B4B0B4DD3F861 +:1078F000801029401160C3F88050D3F88410516083 +:107900004FF0FF32C3F88420047006B962B670BD30 +:107910000370FAE7409D002000E100E0FC06FFBD97 +:1079200010B5084B9A685AB150B9EFF3108172B68E +:10793000054A1C6814605C685460986001B962B6BE +:1079400010BD00BF409D002000E100E003462AB1C9 +:1079500010881A4619448A4203D170474FF6FF70C7 +:10796000F7E712F8013B40BA80B25840C0F3031366 +:10797000584080EA0033580100F4FF509BB2584051 +:10798000E9E70000064B074A00201870064B1A6012 +:107990000822C3E90120C3E90300C3E905007047D9 +:1079A0004C9D0020509D002030B0002000207047EA +:1079B00030B5F9B1124B5C6800220A60E4B1B0F551 +:1079C000167F1BD8D868013C01305C60D8601C6809 +:1079D00018694FF4177505FB00440C60012101FA8A +:1079E00000F49969013000F00700214318619961A2 +:1079F000104630BD0E20FCE70420FAE70C20F8E723 +:107A000030B00020F0B51C498A689AB34D690E6801 +:107A1000AC1A04F0070423464FF4177707FB036CF6 +:107A2000604511D1012000FA03F58869684088613A +:107A300000204E68D1F818C04FF0010E73440025A5 +:107A4000164403F007030AE0013303F007039D42E5 +:107A5000E4D11020EDE74AB1013A1C4601250EFAA7 +:107A600004F414EA0C0FA6EB0207F4D00DB1C1E93F +:107A70000172F0BD0420FCE730B00020064A136913 +:107A80001268013B4FF4177103F0070301FB032356 +:107A9000C3F858020020704730B0002030B5C0B1A4 +:107AA000B9B10E4BDA68B2B1013ADA609A681C6873 +:107AB00001329A605A694FF4177505FB024404605D +:107AC0000132D4F85802086002F007025A6100201F +:107AD00030BD0E20FCE70420FAE700BF30B00020E4 +:107AE0003FB40C49086890B10B4B1C687CB10B4A41 +:107AF0001568CDE9025000238DF804300B60136047 +:107B000004AB13E90700234604B030BC184704B0A7 +:107B100030BC704754B0002058B0002064B0002042 +:107B2000DC2810B509D0DD2810D0C02816D1FFF709 +:107B3000D7FF0E4B0E4A1A6010BD0E4A0E4B196845 +:107B40001368581C1060C022CA54F2E7094A0A4B55 +:107B500019681368581C1060DB22F5E7064B054ACC +:107B6000196813685C1CC8541460E2E74484002060 +:107B7000917B0F0054B0002064B00020C02802BFE9 +:107B8000014B024A1A60704744840020917B0F0029 +:107B9000C02810B409D0DB280BD0094B094A19685A +:107BA00013685C1CC854146006E05DF8044BFFF7D2 +:107BB00097BF054B054A1A605DF8044B704700BF3C +:107BC00064B0002054B0002044840020217B0F00CA +:107BD00007B501228DF807000DF10701002002F022 +:107BE00085FD00280CBF0420002003B05DF804FBD5 +:107BF00010B5064A064C12682368D05CFFF7E8FF10 +:107C000010B923680133236010BD00BF68B00020A5 +:107C10005CB0002008B5C020FFF7DAFF28B9034B9D +:107C20001B6813B9024B034A1A6008BD5CB0002000 +:107C300048840020F17B0F0008B5DB20FFF7C8FF68 +:107C400010B9024B024A1A6008BD00BF48840020E8 +:107C5000557C0F0010B50C4A0C4C12682368D35C9D +:107C6000C02B03D0DB2B0DD0042010BDDC20FFF790 +:107C7000AFFF0028F9D12368054A01332360054B83 +:107C80001A60F2E7DD20F2E768B000205CB0002067 +:107C9000F17B0F00488400207FB5184C184D194E19 +:107CA000002002F0C3FC30B322689AB1296833681F +:107CB00099420FD2012201A9002002F0C3FC10B9A1 +:107CC0004FF0FF3001E09DF804000F4BC0B21B687D +:107CD0009847E5E70D4B1B686BB10292084A0221F9 +:107CE000126803928DF8041004AA12E9070004B088 +:107CF000BDE87040184704B070BD00BF64B00020FC +:107D000054B0002050B000204484002058B000201F +:107D1000014B18600020704758B00020034B1A78C0 +:107D20000AB901221A700020704700BF4CB0002031 +:107D3000014B0020187070474CB000202DE9F04F27 +:107D400085B0002851D02A4F3B78012B07D0022B59 +:107D500014BF08240424204605B0BDE8F08F254D4B +:107D6000DFF8A090244E254CDFF89C80DFF89CA023 +:107D7000DFF89CB0C9F8001000232B6002233060AC +:107D80003B70C4F800802A68D9F800309A4215D3B5 +:107D9000C4F80080FFF73EFF044608BB184B1B6881 +:107DA00001223A70E3B18DF80420326802922A6809 +:107DB000039204AA12E907009847CCE733682A68BF +:107DC0009A5CC02A03D02A689B5CDB2B04D1236811 +:107DD000534508BFC4F800B023689847042801D170 +:107DE0000024B8E71128CED1FAE71024B3E700BF8A +:107DF0004CB000205CB0002068B000204884002017 +:107E000058B0002060B00020157C0F00F17B0F00FF +:107E1000397C0F00054B064A1860064B1960064B6B +:107E200000201860054B1A60704700BF64B0002046 +:107E30007D7B0F0050B0002054B00020448400200F +:107E4000064B07481B68DB00DBB2002203705B4275 +:107E500042708270C3700421FFF770BF94B000209D +:107E60006CB0002070B52B4C2B4D02462378012BB3 +:107E700014D0022B21D0002B4BD1002A49D1274806 +:107E8000FFF752FC08B1FFF719FD254B1B68002BCB +:107E90003FD0244ABDE8704010781847012A38D1F5 +:107EA0002968214B06311868FFF748FF08B1FFF732 +:107EB00005FD022323700022D8E7022A16D0032AE8 +:107EC0000CD032BB194B15481A6041F67F21FFF7E1 +:107ED000E3FBF0B1BDE87040FFF7F0BC144A136853 +:107EE000013303F0070313600023E3E70F4A13682D +:107EF000052B0AD001331360074B19680A4BBDE804 +:107F0000704018680631FFF719BF064B01221A703E +:107F1000EAE770BDB8B00020ACB0002070B000201F +:107F2000A8B00020B0B00020C0B00020B4B0002045 +:107F300098B00020F0B585B004AB03E907009DF8C8 +:107F40000400032874D8DFE800F00802A3A601208B +:107F500005B0BDE8F040FFF785BF039E544C032EEB +:107F600040F28280029D6B7813F00F0265D00E2ADA +:107F70007AD1042E55D02A78500652D5110650D504 +:107F80001A44AB781A44EB781A4412F0FF0248D135 +:107F9000B71E39462846FFF7D9FCEB5B834240D138 +:107FA00044492A780B6802F00702D8B282422BD1EA +:107FB000013303F007030B60FFF742FF3E4B012242 +:107FC00030461A70FFF75AFD08B1FFF777FC3849C1 +:107FD0004FF41670FFF7ECFC002862D0042802D0A2 +:107FE0000020FFF76BFC35480521FFF713FF08B1B0 +:107FF000FFF764FC324B1B68002B56D04FF000009B +:1080000005B0BDE8F040184720684FF41671FFF73F +:1080100001FF08B1FFF752FC05B0BDE8F040FFF7E3 +:108020000FBF20684FF41671FFF7F4FE00283CD014 +:1080300005B0BDE8F040FFF741BC2978AA780B44B1 +:108040001344EA78134413F0FF030DD11D4A12685C +:108050000132C1F3C20102F00702914204D11A4A6F +:1080600003201370FFF7FEFE25681DB14FF4167153 +:108070002846D9E70E494FF41670FFF799FC60B116 +:10808000042802D02846FFF719FC0C480521CBE74D +:108090000A480521C8E70320CAE720684FF4167193 +:1080A000C2E720684FF416719FE705B0F0BD00BF2E +:1080B000BCB0002094B0002090B000209CB0002004 +:1080C000A4B0002098B00020B0B000200220FFF73C +:1080D000C9BE0000074B10B5044618600648FFF7FC +:1080E00017FE08B1FFF7EAFB002C0CBF0E200020A2 +:1080F00010BD00BFA4B00020357F0F00184A1948FA +:10810000002310B51360184A1360184A1360184A08 +:108110001370184A1370184B184A01211960184B34 +:108120001960184B1970FFF7A5FA08B1032010BDAC +:10813000FFF728FC0028FAD1FFF7F0FD0028F6D160 +:10814000114C4FF416702146FFF732FC0028EDD198 +:1081500020684FF41671BDE81040FFF75BBE00BF0A +:10816000C0B00020CCBB0F00ACB00020B4B00020E9 +:1081700090B00020B8B0002094B00020CD800F0057 +:1081800098B00020B0B00020BCB000200C4A08B568 +:10819000002313600B4A1360FFF708FC08B1FFF7D8 +:1081A0008DFBFFF7C5FD08B1FFF788FB0648FFF719 +:1081B000BBFA042802D10020FFF780FB002008BD95 +:1081C000A8B00020A4B0002070B0002037B50D4644 +:1081D000044698B191B10A4B19780022019259B125 +:1081E00001A91A70FFF75AFC019B063B2B802368FC +:1081F0000433236003B030BD0420FBE70E20F9E711 +:1082000090B000200438FFF7FDBB18DF7047000076 +:10821000F0B51D46154B87B018680F4659681B7A94 +:1082200003AC03C42370124B18685968114B0093B8 +:1082300001AC03C42046164603F042FA214602462A +:10824000384603F093F801A803F03AFA01A9024670 +:10825000304603F08BF8684603F032FA694602466E +:10826000284603F083F807B0F0BD00BFD0BB0F0075 +:10827000D9BB0F00312E30000120704710B51C46CD +:108280000B781E2B0AD000232022052102F07AFC55 +:108290004FF6FF70A04228BF204610BD0020F9E72E +:1082A000F8B5069F14460D463A46002118461E466C +:1082B000FCF772F87CB14FF0E023D3F8F03DDB0718 +:1082C00000D500BE4FF0FF300AE0284600F0F8F974 +:1082D000013504F50074BC4206EB0401F5D32046D9 +:1082E000F8BD0000F8B50A4F0D461E460024069B57 +:1082F0009C4206EB040101D32046F8BD3A462846CD +:1083000000F0B4FA0028F7D0013504F50074EEE768 +:10831000C4B0002030B5264D2A7A8DB09AB107AC92 +:108320001422002120460625FCF736F88DF81C5053 +:108330000B9B009394E80F00FCF7CAFF0620FCF7A4 +:10834000F7FC0DB030BD2B68002BFAD0194B197813 +:1083500019B105201A70FCF7EBFCD5E900329A42FE +:10836000EFD307AC142200212046FCF715F86B7AF6 +:10837000DBB106234FF420424FF474214FF4602008 +:108380008DF81C3002F0C0FF0028D1D0102200214F +:1083900003A8FCF701F84FF460224FF4205303A820 +:1083A000CDE90423FFF731FFC2E78DF81C30BFE7AA +:1083B000C4B000204C840020024B0B604FF40073CB +:1083C0001380704709010100012070470048704781 +:1083D000FA840020044B054A1878054B002814BF86 +:1083E00010461846704700BFE0B20020AF8400205E +:1083F0004D8400202DE9FF411E4B187020B11E4B0B +:108400002A229A720022DA721C4A1D4DDFF878C0C7 +:1084100017461C4BEE4603F110067446186859685F +:10842000F046A8E803000833B342C646F6D12B78DD +:1084300003F00F0310336B4413F8103CD373114B4C +:1084400018685968A646AEE803000833B34274467C +:10845000F6D115F8013B04A901EB1313654513F898 +:10846000103C9373A2F10202D3D100233B7404B0F9 +:10847000BDE8F081E0B20020FA84002064B300205F +:1084800060000010E1BB0F006800001010B570B96B +:10849000134B14481968022202F068FF01230133CC +:1084A000DBB211485B0043F44073038010BD052824 +:1084B00014D80B4B53F82040204603F001F9C3B207 +:1084C0001F2B28BF1F23084A2046E118884202F1CB +:1084D0000202E4D010F8014B1480F7E70020E5E732 +:1084E0000C850020E4B20020E2B200204DDF70478E +:1084F0004EDF70474FDF704750DF704712DF704725 +:1085000000F0C8BE002000F033BD00001FB5244BB2 +:10851000402283F8272300238DF807304FF440537F +:1085200004465A681F4B9A4227D10DF10700FFF706 +:10853000E5FF9DF8073003B30120FFF7D9FF0120C5 +:10854000FFF7D4FF0120FFF7D5FF02A8FFF7D4FF04 +:10855000029BDA0702D5002000F09CFE029B9B07DD +:1085600002D5022000F096FE2046FFF743FF00F000 +:1085700027F802F059FE04B010BD002301A88DF8C1 +:108580000430FCF721FC084B039303A8FCF744FCE0 +:10859000FCF748FC4FF08043D3F838340293D7E718 +:1085A00000E100E0DBE5B15101850F00012000F0A2 +:1085B00071BE0120FCF7BCBB0220FCF7B9BB000078 +:1085C0007FB52F492F4802F0E7FF4FF440532E4A62 +:1085D000596891424CD11A78102A46D9142A186940 +:1085E00044D95B69294CB3FBF4F50A2201A904FBC9 +:1085F000153403F021F92649224802F0CDFF01A9E4 +:10860000204802F0C9FF23491E4802F0C5FF0A2294 +:1086100001A9284603F010F901A91A4802F0BCFF8D +:108620001D49184802F0B8FF4FF47A760A2201A9D2 +:10863000B4FBF6F5284603F0FFF801A9114802F053 +:10864000ABFF15490F4802F0A7FF0A2201A906FB5C +:10865000154003F0F1F801A90A4802F09DFF0F4907 +:10866000084802F099FF04B070BD00200023B9E76C +:108670000B49044804B0BDE8704002F08DBF00BF54 +:10868000FFBB0F0024850020DBE5B15140420F0005 +:108690000CBC0F000ABC0F000EBC0F0019BC0F0071 +:1086A00010BC0F0010B503461C1A944200DB10BD2D +:1086B0000C781CB1013103F8014BF5E72024FAE7EF +:1086C0002DE9F3410C4605464FF400720021204687 +:1086D000FBF762FE6DB95E493E22204602F046FE7F +:1086E000552384F8FE31AA2384F8FF3102B0BDE897 +:1086F000F081B5F5017F2DD8691EB1F5817F24BFCA +:108700006FF4817805EB0801C9B10B02C1EBC151CF +:1087100003F5807004EB412440F693651A1FB2F50F +:10872000696F03F1010206D2AB4214BF91B24FF65A +:10873000FF7124F8131090421346EFD1D6E7F823C7 +:10874000237004F109022346FF2003F8010F93422E +:10875000FBD1DAE7B5F5027F3BD86FF4017C6544C5 +:108760003DB920463B490B22FFF79CFF2823E372CB +:10877000203439492E014FF0000801EB05256FF038 +:108780001F07022EB2D80B2229462046FFF78AFF88 +:108790005923212269216374E3746376B31C84F83E +:1087A0000D80A773E1732274A27484F8148084F896 +:1087B0001580A775E17522766383E86830B102F011 +:1087C0007FFFE061013620341035DAE74FF4E9101D +:1087D000F7E7224B9D4289D86FF40277EA19012A04 +:1087E0000FD81D4B03EB0213D9680191084602F024 +:1087F00067FF01990246204602B0BDE8F04102F051 +:10880000B5BD6FF4FD76A9190902B1F5801FBFF45B +:108810006DAF134B236003F1144303F52C1303F6E0 +:10882000023363600F4BC4F8FC314FF46963A361FA +:108830004FF40053A5F20B254FF48072A3600A4B4E +:108840006561E1602261E36104F12000D4E700BFCB +:108850001CBC0F0047BC0F00D4BC0F000801010076 +:108860005546320A306FB10A29009A23F7B5654B95 +:1088700014460A689A420D4639D103F114434A68F6 +:1088800003F52C1303F602339A4230D1D1F8FC21C0 +:108890005D4B9A422BD18B6823F4FF5323F01E03C8 +:1088A0009B049B0CB3F5005F21D10B69B3F5807F6E +:1088B0001DD1C86810F0FF0619D1CB69534A934205 +:1088C00005D0534A934215D0524A93420FD1A0F596 +:1088D0008053B3F5692F07D201234FF4807205F15D +:1088E0002001FBF705FF22E0B0F5805F1FD34FF0BA +:1088F000FF3021E001276772CB68B3F1102F1DD143 +:1089000004223431684602F031FD042205F13801B9 +:108910000DEB020002F02AFD009BB3F5742F03D18A +:10892000019BB3F57E2F01D02772E0E7A772AB69F8 +:10893000002B37D14FF4007003B0F0BDA3F57422C3 +:10894000B2F5204F28D2E27A01F12007DAB9324A93 +:10895000934218D32B69B34215D90422B91968463A +:1089600002F004FD009BD02B14D10422311D0DEB2D +:108970000200394402F0FAFC264B019A9A424FF069 +:1089800001030DD1E372E8682A6901233946A0F595 +:10899000A030A6E70836DDE7B3F5805FC7D3012333 +:1089A0002372A4E72268934207D041F263018B420D +:1089B00000D80AB14FF0FF3323606B6941F26302C4 +:1089C0009342B7D803F0070204EBD303012191408F +:1089D0001A7B1142C8B204D16168024301311A7393 +:1089E0006160D4E900329A42A4D30120FBF762FE11 +:1089F000637A002B9ED0A37A002B9BD10123237294 +:108A000098E700BF5546320A306FB10A4028A5AD3D +:108A10003C8263D629009A2300D80F004FF0805380 +:108A2000D3F83001082802D1D3F8343123B9A0F1AA +:108A30000D0358425841704701207047094B0122ED +:108A400083F8D8200260BFF36F8FBFF34F8F064AC1 +:108A5000904202D0043A904202D1002283F8D820FA +:108A6000704700BF78B300205070024042DF70476B +:108A700043DF704744DF704712DF704710B5134B78 +:108A8000134A5B68C3F3080373B9EFF310835BB950 +:108A900010494B681B0607D58024C1F8844092F822 +:108AA000D8307BB14C60F8E792F8D83033B101464A +:108AB000BDE810400848012201F094B8BDE810401C +:108AC000FFF7BCBFFFF7BAFF4C6010BD00ED00E040 +:108AD00078B3002000E100E07D8A0F000D4B1822E2 +:108AE00002FB0033598A1A8A521A998A92B28A4230 +:108AF00028BF0A46D9681423434303F1804303F592 +:108B00001C33C3F80016C3F80426034B03EB8000A4 +:108B1000FFF7B4BF78B300200470024007B54FF4EC +:108B2000405300205A68084B9A420AD18DF807003A +:108B30000DF10700FFF7A0FF9DF80700003818BFF0 +:108B4000012003B05DF804FBDBE5B15107B5FFF789 +:108B5000E5FF58B1002301A80193FFF78BFF0198AF +:108B6000003818BF012003B05DF804FB4FF08043CC +:108B7000D3F80C0400F00110A0F101135842584141 +:108B8000F1E70000074BD3F8C024D10309D406490C +:108B90000648D1F8C010C3F8A017C3F8A427FFF700 +:108BA0006DBF70470070024078B3002048700240EB +:108BB0000828F0B402D1F0BCFFF7E4BF0F4D104A13 +:108BC000182141436E1800F59473695852F82340F8 +:108BD000F788B388DB1B9BB2A4B2A34228BF23460D +:108BE000142404FB0022C2F80017C2F80437054B16 +:108BF000F0BC03EB8000FFF741BF00BF78B300205B +:108C0000007002402870024070470000014B802233 +:108C10005A60704700E100E0024B8022C3F88420D4 +:108C2000704700BF00E100E0074BD3F80014D3F811 +:108C300000240A43C3F800240022C3F858214FF44B +:108C40008002C3F8042370470070024070B5887832 +:108C5000404D00F07F0318220C26C4095A4306FB3E +:108C600004228E882A44C6F30A061681CA7802F0C6 +:108C70000302012A29D00121374A01FA03F5D4B9A8 +:108C800003F10C06B140C2F80413D2F8141503F531 +:108C900094732943C2F8141542F823402E4BC3F8AD +:108CA000180540F48070C3F80C05BFF36F8FBFF355 +:108CB0004F8F012014E002339940C2F80413D2F818 +:108CC00010352B43C2F81035E8E7082B09D04FF0D8 +:108CD000E023D3F8F00D10F0010001D000BE002019 +:108CE00070BD1D4BDCB9B5F8D42012B18022C3F899 +:108CF0001C250022C3F85021D3F8002312F40012DF +:108D000008BFC3F85421144B4FF44012C3F8042396 +:108D1000D3F8142542F48072C3F81425BEE700226C +:108D2000C3F82C21B5F8C82012B18022C3F81C2545 +:108D3000094BD3F8002312F4001208BFC3F85421E2 +:108D4000064AC3F80423D3F8102542F48072C3F80E +:108D50001025A3E778B3002000700240000820002F +:108D60001D4B2DE9F041802201241C4DDFF8788055 +:108D7000C3F88420274604F10C03A21C07FA02F270 +:108D800007FA03F31343C5F80833A30003F1804344 +:108D900003F51C330026182202FB04805E60314676 +:108DA0009E620134FBF7F8FA082C4FF01802E2D16A +:108DB0000B4BC5F808330B48C5F81C6531466E628D +:108DC000AE64FBF7E9FA044BC5F814758022C5F8C8 +:108DD00010755A60BDE8F08100E100E000700240CB +:108DE0000008300038B4002078B30020F7B51F46E3 +:108DF00001F07F0018231F4CCD090E4643430C2180 +:108E000001FB053104EB010C62500022ACF8047048 +:108E1000ACF8062018B1F5B1FFF760FE18E017BBFB +:108E2000154BD3F88034C3F3C0139D421BD01348B5 +:108E3000FFF724FE124B5B68C3F30803003B18BF27 +:108E4000012300933A463B463146384600F0B5FED2 +:108E5000012003B0F0BD1C44A37A002BF8D0A5720A +:108E6000FFF7A6FEF4E7002DD6D10648FFF706FE71 +:108E7000EEE700BF78B3002000700240507002405F +:108E800000ED00E04C70024011F07F0008B507D102 +:108E90000D4B01225A65BFF36F8FBFF34F8F08BD93 +:108EA0000828F8D0084B41F48072C909C3F8182586 +:108EB000F1D1064B182202FB00339A7A002AEAD03D +:108EC0009972FFF775FEE6E70070024078B3002064 +:108ED00011F0770F12D00A4B41F48072C3F80C15D1 +:108EE000C3F80C25CA09C3F8181504BF01F594711D +:108EF00043F82120BFF36F8FBFF34F8F704700BF40 +:108F000000700240174B0122002110B5C3F8142550 +:108F1000C3F810250A468B0003F1804303F51C3388 +:108F2000013108295A609A62F5D10E4B0E4C5A62F3 +:108F30009A64C3F85821D3F80014D3F800240A43E4 +:108F4000C3F80024D3F80023C3F80823074AC3F862 +:108F500004230021DC222046FBF71EFA4023A382D3 +:108F6000238110BD0070024078B300200514C001B9 +:108F70002DE9F04FB24BB34AD3F80013002385B06C +:108F80001C4601201D4621FA03F6F6070BD552F8C0 +:108F9000236046B100FA03F6344342F82350BFF38E +:108FA0006F8FBFF34F8F0133192BECD1E20706D53A +:108FB000FFF7A8FF00210122084600F0D5FD14F4B8 +:108FC000006FA14D08D09E4BD3F8A8369BB2A5F8F0 +:108FD000D230012385F8D730A30221D5984ED6F898 +:108FE000143513F4807302D0FFF7CCFD0123D6F8BB +:108FF0001025D70540F1188195F8D7305BB1B5F849 +:10900000D2200023012185F8D73092B20091184672 +:10901000882100F0D2FD01220321002000F094FD00 +:10902000660228D5864BD3F8006406F4E062F005AA +:10903000C3F8002406D50122C3F82C250421002002 +:1090400000F082FD71050FD57D4B0122C3F8082584 +:109050009A65D3F8002312F4001208BFC3F8542114 +:109060004FF40012C3F80423B20504D501220521F0 +:10907000002000F069FD23022BD5714BD3F880143A +:10908000C9B28DF80810D3F88424D2B28DF8092023 +:10909000D3F888048DF80A00D3F88C048DF80B00FF +:1090A000D3F890048DF80C00D3F894048DF80D00DB +:1090B000D3F898048DF80E00D3F89C348DF80F3057 +:1090C0004F0601D1052A04D0012202A9002000F098 +:1090D0005EFD5E4B23405BB195F8D830002B40F02D +:1090E000AB804FF0E023D3F8F03DDE0700D500BEA3 +:1090F000DFF85481DFF84891DFF858B14746002681 +:109100004FF0140A06F10C0324FA03F3D807F1B266 +:1091100022D50AFB0693082ED3F808273B6853FA9A +:1091200082F33B604FF0180303FB0653D2B2D8889A +:1091300012FA80F080B2D88000F08E803889904298 +:1091400040F08A80DB88BA889BB29A4240F28480E1 +:1091500011B95846FFF792FC0136092E07F118079E +:10916000D0D13B4B2340002B5BD0354BD3F86C94D4 +:10917000C3F86C94BFF36F8FBFF34F8F14F4806408 +:1091800007D0D3F88044D3F880341906C4F3C01450 +:1091900070D54FF0000A2C4F00264FF0180B29FA1B +:1091A00006F3DA07F0B201D4CEB9C4B1244B1422CD +:1091B00002FB0633FA68D3F8083652FA83F2FA60F3 +:1091C0000BFB0652518A89B251FA83F39BB2538248 +:1091D000538A398A9BB299424FD9FFF77FFC0136F7 +:1091E000082E07F11807DAD100241826012704F108 +:1091F000100329FA03F3DB07E0B203D464B9BAF130 +:10920000000F09D006FB0452B8F80410D3889BB2B3 +:1092100099423DD9FFF7CCFC0134082C08F118081D +:10922000E5D105B0BDE8F08F002B7FF4F4AE4FF42C +:109230000013C6F80833EEE6002385F8D83057E768 +:10924000007002400071024078B30020FCFB1F0058 +:10925000000400014C700240182303FB0653DA8817 +:10926000BA80DA8801230093002392B2184600F0F6 +:10927000A4FC71E74FF0010A8DE7528A01230093A5 +:10928000002340F0800192B2184600F096FCA6E759 +:109290009772C1E7012813B5044600F0C380022885 +:1092A00059D0002855D1784BD3F80025002A50D149 +:1092B0004FF48002C3F808234FF40062C3F800247F +:1092C000BFF36F8FBFF34F8FFFF7A8FB60B16F4BFA +:1092D000D3F8001C032269BB49F27531C3F8001CA6 +:1092E000C3F8142DC3F8001C4FF08053D3F830316D +:1092F000082B0CD1654BD3F8001CC022E9B949F208 +:109300007531C3F8001CC3F8142CC3F8001C5E4B65 +:109310000124C3F80045BFF36F8FBFF34F8FFFF7F2 +:1093200015FCB0B9FFF7FAFB50B102B0BDE8104030 +:10933000FFF79CBBC3F8142DD6E7C3F8142CE6E75F +:109340004FF08043C3F80001D3F800210192019A45 +:109350001C6002B010BD4C4CD4F804351BB1FFF7B3 +:10936000F5FB0028F5D1D4F800341B05FBD54FF4EC +:109370000063C4F80034BFF36F8FBFF34F8F4FF01B +:109380008053D3F83031082B0CD1404BD3F8001C5C +:1093900000293FD149F27532C3F8002CC3F8141CE0 +:1093A000C3F8002CFFF73AFB58B1384BD3F8001C38 +:1093B000A1BB49F27532C3F8002CC3F8141DC3F8E1 +:1093C000002C4FF08053D3F83031082B2E4B0AD1AC +:1093D00040F2E372C3F800284022C3F80428BFF328 +:1093E0006F8FBFF34F8F80220121C3F81C25C3F874 +:1093F0000413274BC3F884215A60FFF7A7FB00280A +:10940000FBD0214B0122C3F80425BFF36F8FBFF3BC +:109410004F8F9EE70022C3F8142CC3E70022C3F845 +:10942000142DCEE7184BD3F80025002A91D0002246 +:10943000C3F80425BFF36F8FBFF34F8F144980200B +:10944000C1F88400D3F80013C3F80813C3F800254B +:10945000BFF36F8FBFF34F8FFFF760FB78B1FFF75C +:1094600007FB0C4B5A68C2F30802003A18BF0122EE +:109470000221002002B0BDE8104000F065BB4FF0B3 +:1094800080435C60EDE700BF0070024000E00640F2 +:1094900000E100E000ED00E00A44034690B288429B +:1094A00002D39A89824202D25A89104480B270470C +:1094B00082888A4210B504D884898B1A9BB29C4258 +:1094C00003D243891A44891A8BB2038210BD9308D0 +:1094D00013B501EB8303044699420BD112F003024A +:1094E00006D0002301A8019301F040FF019B2360F7 +:1094F00002B010BD51F8040B2060EDE72DE9F043F8 +:1095000085B01446BDF83050AB4238BF4289A3EB5A +:1095100005091FFA89F938BFA9EB0209828838BF0B +:109520001FFA89F94A4588460746194605D2FFF7CA +:10953000BFFF058AB0F80490ADB2B9F1000F1FD09B +:10954000A14528BFA146BC88AC421DD9FA889DF828 +:1095500034003968661BA9EB04042C44B3B214FB35 +:1095600002F416FB02F60128B6B2A4B202FB051102 +:1095700016D099450BD802FB09F2404601F0F6FEE1 +:10958000484605B0BDE8F0832D1BADB2DCE732469E +:10959000404601F0EBFE3968224608EB0600EDE795 +:1095A000994506D819FB02F292B24046FFF78FFFA9 +:1095B000E6E726F00305ADB22A4640460191FFF7E3 +:1095C00086FF16F0030628D001990D44C6F1040168 +:1095D00089B2A1424FF0000328BF2146641A0393C9 +:1095E00003ABA4B2A8191A4685420CD13B68013ED0 +:1095F000164419448B420BD1039BC8F80030002C51 +:10960000BED02246D1E715F801CB03F801CBEBE73A +:1096100013F8012B06F8012FECE73968EFE713B5D3 +:10962000930800EB8303984209D112F0030204D09F +:109630000B68019301A901F099FE02B010BD0C68FE +:1096400040F8044BEFE770B59A42A2EB03041D46C5 +:1096500038BF4389A4B238BFE41A838838BFA4B2A4 +:10966000A3420E46114602D2FFF722FF848874B14E +:109670008288AA4208D9C2880168304602FB0511D7 +:1096800001F074FE012070BDAD1AADB2F1E72046C5 +:10969000F9E72DE9F0430746B0F80E908588048A73 +:1096A0001646C288007A85B01FFA89F9A4B288BB31 +:1096B000A145A9EB040038BF7C8980B23CBF001BE8 +:1096C00080B2281A80B2864228BF06464C46AC4279 +:1096D00028D2A5EB0408751B386825441FFA88FCBE +:1096E00015FB02F518FB02F8012B1FFA88F8ADB242 +:1096F00002FB040022D0664517D8724301F036FE03 +:10970000324649463846FFF7C7FEF881304605B075 +:10971000BDE8F083AE4221BF761B02FB0611A146D5 +:109720002E46D3E7641BA4B2D1E74246009101F074 +:109730001DFE009938682A464144DFE7664505D892 +:1097400016FB02F292B2FFF76AFFD9E728F0030492 +:10975000A4B22246CDE90001FFF761FF18F003082B +:10976000019929D0C8F104039BB2AB4200980A6862 +:10977000039228BF2B46013CED1A0DF10C0C20443E +:10978000ADB244466246013C1CF801EB00F801EF23 +:1097900014F0FF04F7D13868904408EB0304421E2C +:1097A000A04504D11844002DAAD02A46CBE718F8CA +:1097B00001CB02F801CFF3E73868F4E7B2F5004FC8 +:1097C00010D882805200C38092B29DF8003003729C +:1097D000531E838152420023C38101604281038270 +:1097E0000120704700207047C189028A89B292B275 +:1097F0009142A1EB020338BF428980889BB23CBFF3 +:109800009B1A9BB2984228BF18467047C289038AA8 +:1098100092B29BB2D31A58425841704710B5C189D1 +:10982000028A848889B292B2914238BF4089A1EB02 +:1098300002039BB23CBF1B1A9BB2E01A80B210BD60 +:1098400038B5C289038A04469BB292B2FFF7FBFE89 +:10985000218A054682B289B22046FFF71DFE20828A +:10986000284638BD73B5C389058A0026ADB20446C3 +:10987000CDE900569BB2FFF741FE218A054602461C +:1098800089B22046FFF708FE2082284602B070BD4C +:1098900038B5C589028AADB292B2AA42A5EB0203DD +:1098A00088BF42899BB288BF9B1A828888BF9BB2BF +:1098B0009A42044614D1007A90B938BD9B1A9BB2E3 +:1098C0009342FBD2E288206802FB030001F04EFDC8 +:1098D000012229462046FFF7DFFDE0810120ECE769 +:1098E0002B46EDE712B10023FFF7D3BE10467047B9 +:1098F0000023C381038283885B009BB25A1E5B42B4 +:10990000828143810120704701720120704700006D +:109910000B4B63B10B4B1B78834206D90A4B1B6878 +:1099200000EB400003EBC0007047C01AC0B2012832 +:1099300003D8064B00EB4000F4E70020704700BF5F +:109940000000000058B4002054B4002004BD0F00F3 +:1099500070B5104E054600242046FFF7D9FF436836 +:109960002846984733780134E4B20133A342F3DA4E +:10997000372200210848FAF70FFD1022FF2107487F +:10998000FAF70AFDBDE8704005481222FF21FAF7F8 +:1099900003BD00BF58B4002059B400205CB40020BF +:1099A0006CB4002037B50C460546C868019200F03B +:1099B000A5FDE368019A0021284603B0BDE83040C8 +:1099C000184773B5054614463AB90378012B04D1FC +:1099D00010460191FFF720F90199281DFFF758FF64 +:1099E00006462CB92B78012B02D12046FFF70EF941 +:1099F00036B94FF0E023D3F8F03DDB0700D500BEC9 +:109A000002B070BD024B5878003818BF0120704773 +:109A100059B40020024B1878C0F38000704700BF93 +:109A200059B40020014B1878704700BF90B4002053 +:109A3000F8B5164E3178054629BB154C1548372226 +:109A4000FAF7AAFC201DFFF753FF134B1C60134BC2 +:109A500023B11348AFF30080124B1860104F00245D +:109A60002046FFF755FF036898473B780134E4B27E +:109A70000133A342F4DA2846FFF7C6F82846FFF779 +:109A8000C5F8012333700120F8BD00BF90B4002059 +:109A9000A486002059B4002094B4002000000000E7 +:109AA00058B4002054B400201FB54378023B0A4646 +:109AB000032B12D8DFE803F0022A1921204B197872 +:109AC0006FF300011970197800246FF341011970C8 +:109AD0005C70197864F3820119701A4B014618689A +:109AE00004B0BDE81040FFF76CBF154B1978C907EB +:109AF00023D5197841F00401EEE711490B78DC0712 +:109B00001BD50B786FF382030B70E6E70C490B78DB +:109B10005B0712D50B786FF382030B700023CDE93E +:109B20000133039303788DF8043005238DF8053055 +:109B3000044B01A91868FFF744FF04B010BD00BF33 +:109B400059B4002094B400201FB50023CDE901339F +:109B50008DF804008DF8051001A811460393FFF756 +:109B6000A3FF05B05DF804FB1FB50023CDE9013369 +:109B700003938DF8040001238DF8081001A8114605 +:109B80008DF80530FFF790FF05B05DF804FB1FB5B9 +:109B9000144600230822CDE9013303938DF8040015 +:109BA00006230DEB02008DF8053001F0DFFB2146A6 +:109BB00001A8FFF779FF04B010BD1FB50024CDE95F +:109BC00001448DF8040007208DF805008DF8081079 +:109BD00001A89DF8181003928DF80930FFF764FF73 +:109BE00004B010BD1FB54FF40063CDE901300391FF +:109BF00001A81146FFF758FF05B05DF804FB00000F +:109C000038B58B7803F07F03082B05460C4608D93E +:109C10004FF0E023D3F8F03DDB0700D500BE002075 +:109C200038BD064B2046997801F00DFB0028EFD097 +:109C300021462846BDE83840FFF708B859B400204F +:109C40002DE9F047DDE9085681460C4690469A46D4 +:109C50000027B84501DC01200EE06378052B04D114 +:109C6000E37803F0030353450AD04FF0E023D3F821 +:109C7000F03DDA0702D40020BDE8F08700BEFAE725 +:109C800021464846FFF7BCFF38B94FF0E023D3F830 +:109C9000F03DDB07EFD500BEEEE7A378DA0914BF8D +:109CA00033702B70237801371C44D2E70B4B01F043 +:109CB0007F0203EB420303EBD1112031487910F00E +:109CC000010008D14B795B0706D44B7943F00403BC +:109CD0004B71012070470020704700BF59B400202D +:109CE0000B4B01F07F0203EB420303EBD111203158 +:109CF0004B7913F0010209D14B79C3F380005B0764 +:109D000005D54B7962F382034B7170470020704791 +:109D100059B4002070B5164D01F07F0605EB4605DD +:109D200005EBD11420346579ED0709D54FF0E02318 +:109D3000D3F8F03DDA0701D4002070BD00BEFBE788 +:109D4000657945F001056571FFF750F80028F4D1F9 +:109D5000637960F300036371637960F38203637175 +:109D60004FF0E023D3F8F03DDB07E5D500BEE4E794 +:109D700059B40020054B01F07F0203EB420303EBD3 +:109D8000D11191F8250000F00100704759B400206E +:109D900010B50B4B01F07F0203EB420303EBD11430 +:109DA000203463799B0709D4FFF76EF8637943F099 +:109DB00002036371637943F00103637110BD00BF57 +:109DC00059B4002010B50B4B01F07F0203EB4203A6 +:109DD00003EBD114203463799B0709D5FFF778F89A +:109DE00063796FF34103637163796FF30003637108 +:109DF00010BD00BF59B40020054B01F07F0203EBFA +:109E0000420303EBD11191F82500C0F340007047E5 +:109E100059B400202DE9F04F87B001F012FA002864 +:109E200000F09182AF4B1D682B78012B02D10020EE +:109E3000FEF7F2FE03A9281DFFF702FD2B78012B88 +:109E4000044602D10020FEF7E1FE002C00F07B82E8 +:109E50009DF80D30013B072B00F2B382DFE813F0D1 +:109E600008001300A8027E028D021F004A02AA0207 +:109E70009DF80C00FFF76CFD00F038FB9A4B9DF845 +:109E800010209A70CEE79DF80C00FFF761FD00F0FE +:109E90002DFB964B002BC5D0FEF78EFBC2E7924CF4 +:109EA0009DF80C50237843F00103237094F825307B +:109EB0006FF3000384F8253094F825306FF38203A4 +:109EC00084F8253094F826306FF3000384F82630A8 +:109ED00094F826306FF3820384F82630002000F0D7 +:109EE0000DFB9DF8106006F06002602A11D14FF062 +:109EF000E023D3F8F03DDC0700D500BE9DF80C0050 +:109F00000021FEF7C1FF9DF80C008021FEF7BCFF89 +:109F100088E7402A0DD176480028EFD000F0EEFA0D +:109F200004AA00212846AFF3008000287FF47AAF0E +:109F3000E4E706F01F06012E00F07181022E00F00A +:109F40009881002ED3D1202A0FD19DF814300F2BE9 +:109F5000D4D82344D878FFF7DBFC01460028CDD0C5 +:109F600004AA2846FFF71EFDDFE7002ABFD19DF8AF +:109F70001130092BBBD801A252F823F009A20F001F +:109F8000F7A10F00EF9E0F00E3A10F00EF9E0F005F +:109F9000A59F0F0021A10F00EF9E0F00BF9F0F0094 +:109FA000D59F0F0004A800F0AFFA9DF812102846C4 +:109FB000FEF73AFE237843F00203237032E763781A +:109FC0008DF80A3001230DF10A0204A9284600F099 +:109FD00059FA27E79DF812906378994537D063784E +:109FE0003BB12846FEF7BCFEA6782846FFF7B0FC3A +:109FF000A670B9F1000F2AD009F1FF30C0B2FEF708 +:10A00000E9F910B14378022B08D04FF0E023D3F8E0 +:10A01000F03DDF077FF56BAF00BE68E7C379C3F3A0 +:10A020008012C3F340131B0143EA4213227822F04B +:10A030003002134323704388C31800F109060093CC +:10A04000009BB3420AD82B4B0BB1FEF7B2FA84F84F +:10A05000019004A9284600F003FAE3E673780B2B7D +:10A0600003BF337896F80380F6184FF00108737831 +:10A07000042BCAD1009B9A1B93B201934FF0000BA3 +:10A080001D4B1B785FFA8BF70133BB42BDDB3846B3 +:10A09000FFF73EFC31468368019A8246284698477E +:10A0A0000828024639D9019B834236D3B8F1010F03 +:10A0B00006D1DAF8083011498B4208BF4FF0020888 +:10A0C0000021CBB298451DD83B4631460C48019241 +:10A0D00001F0E9F8084B019A1B7801339F421644BE +:10A0E000AEDD92E794B4002059B40020B9850F008A +:10A0F00000000000B3850F0058B40020A9A70F008E +:10A100006CB40020B078034454FA83F30131D8785A +:10A11000FF287FF47AAFDF70D3E70BF1010BAFE7D5 +:10A12000BDF81200030A5A1EC0B20E2A3FF6E6AE70 +:10A1300001A151F822F000BF75A10F009FA10F00EF +:10A14000C1A10F00FD9E0F00FD9E0F00D5A10F00C5 +:10A150009FA10F00FD9E0F00FD9E0F00FD9E0F00B2 +:10A16000FD9E0F00FD9E0F00FD9E0F00FD9E0F0047 +:10A1700087A10F00FEF72AF91223024604A92846F8 +:10A1800000F080F9D1E6934B002B3FF4B7AEAFF36C +:10A190000080024600283FF4AAAE4388EEE7022B77 +:10A1A00007D1FEF717F900283FF4A1AE4388024615 +:10A1B000E4E7894B002B3FF4A1AEAFF30080F2E758 +:10A1C000BDF81410FEF762F9024600283FF496AE7F +:10A1D0000378D3E7814B002B3FF490AEAFF30080C0 +:10A1E000F2E7BDF81230012B7FF488AE237843F0FC +:10A1F000080323702DE7BDF81230012B7FF47EAEEB +:10A2000023786FF3C303F4E72378C3F340129B086A +:10A2100003F002031343ADF80A300223D3E69DF89E +:10A2200014300F2B3FF66AAE2344D878FFF770FB4B +:10A23000014600283FF462AE04AA2846FFF7B2FBAD +:10A2400000287FF4EFAD9DF8103013F060047FF428 +:10A2500055AE9DF811300A3B012B3FF64FAE00F092 +:10A260004DF99DF811300A2B7FF4F3AE8DF80A40BA +:10A27000A8E69DF8141001F07F03082B3FF637AED7 +:10A2800004EB430303EBD113D87CFFF741FB0746F4 +:10A290002AB100283FF432AE04AA014661E69DF8D7 +:10A2A000113003F0FD02012A08D0002B7FF41FAE0D +:10A2B0002846FFF7A1FDADF80A00AEE7BDF8122071 +:10A2C00022B9012B284612D1FFF77CFD002F3FF465 +:10A2D000A9AD04AA39462846FFF764FB002000F028 +:10A2E0000DF994F82630DE073FF59CADB1E6FFF797 +:10A2F0004FFDEBE79DF81010394B01F07F0403EBA5 +:10A30000440303EBD11393F825006FF3000083F8A7 +:10A31000250093F825006FF3820083F825003CB9EF +:10A32000059B9DF811209DF80C0000F0FBF879E5E5 +:10A33000D87CFFF7EDFA48B94FF0E023D3F8F03DB1 +:10A34000D80700D500BE07B0BDE8F08F0469059BB3 +:10A350009DF811209DF80C00A04763E5204B1A786A +:10A36000D1077FF55FAD1F4A002A3FF45BAD187837 +:10A37000C0F3C000AFF3008054E5194B1B78DA0737 +:10A380007FF550AD184B002B3FF44CADAFF3008080 +:10A3900048E5FFF7BDFA436913B19DF80C009847F3 +:10A3A0000134124B1B78E0B201338342F1DA39E514 +:10A3B0000024F6E7049B002B3FF434AD0598984742 +:10A3C00030E54FF0E023D3F8F03DDB077FF52AAD11 +:10A3D00000BE27E5000000000000000000000000B3 +:10A3E00059B40020000000000000000058B4002014 +:10A3F00037B514490446CA89888991F90050831AEF +:10A400009BB2402B28BF4023002D10DA904214D07D +:10A410001A4689680C48019300F0A8FF0A4A019B7C +:10A420008021204603B0BDE83040FFF773BC904266 +:10A430004FF0000103D10022F3E78021FBE7024A3D +:10A44000EFE700BF58B500206CB5002011F0800F79 +:10A450004FF000031A460CBF80211946FFF75ABC83 +:10A4600030B4074C05460B4608684968224603C2CB +:10A470000022C4E902222846197830BCFFF7E6BF63 +:10A4800058B50020F8B5184E0C46054608684968CE +:10A49000B260374603C70021F181E1888B4228BFB3 +:10A4A0000B46B381E18889B153B14AB94FF0E0233B +:10A4B000D3F8F03DDA0701D40020F8BD00BEFBE779 +:10A4C0002846FFF795FF30B10120F6E721782846AE +:10A4D000FFF7BCFFF7E74FF0E023D3F8F03DDB07D1 +:10A4E000EAD500BEE9E700BF58B5002002481422B3 +:10A4F0000021F9F751BF00BF58B50020014B18618A +:10A50000704700BF58B5002010B50246044C0068E3 +:10A510005168234603C30023C4E9023310BD00BFC2 +:10A5200058B5002070B52D4C1E462378C909B1EBF3 +:10A53000D31F054618D04EB14FF0E023D3F8F03DBD +:10A54000DA0701D4002070BD00BEFBE7244B13B135 +:10A550002146AFF3008023690BB90120F3E71F4ABE +:10A56000022128469847F8E794F90030002B06DBD3 +:10A57000A0680028E6D01B49324600F0F7FEA2682A +:10A58000E38932443344A260E2889BB29A42E38179 +:10A5900001D03F2E1ED823696BB921782846FFF7DA +:10A5A00055FF0028D9D14FF0E023D3F8F03DDB0769 +:10A5B000C8D500BEC7E70121084A2846984701468A +:10A5C0000028EAD12846FEF75FFC80212846FEF7E6 +:10A5D0005BFCC2E72846FFF70BFFE2E758B5002017 +:10A5E000000000006CB5002070B500F110050446B5 +:10A5F0002846FFF713F93F2817D9E1780020FFF725 +:10A6000055FB90B12846FFF709F93F28E17807D9B3 +:10A6100004F638024023BDE870400020FFF77ABB03 +:10A62000BDE870400020FFF75BBB70BD08B5044B70 +:10A6300040F6B80202FB00301030FFF7D5F808BD35 +:10A64000ACB5002070B540F6B804074E444304F1A1 +:10A65000100092B23044FFF705F905463019FFF7B4 +:10A66000C3FF284670BD00BFACB500202DE9F04106 +:10A670000446FFF7C7F910B90020BDE8F081FFF7E5 +:10A68000C9F906460028F7D140F6B801164D4C43EB +:10A6900004F12408A8444046FFF7A6F80028EBD0B0 +:10A6A0002F193046B978FFF701FB0028E4D004F6F3 +:10A6B00078042544294640224046FFF7D3F8B9786C +:10A6C000044668B103462A463046FFF723FB48B9E3 +:10A6D0004FF0E023D3F8F03DDB07CDD500BECCE74B +:10A6E000FFF7FEFA2046C8E7ACB5002070B50B4C6A +:10A6F00040F6B80303FB0044243492B205462046DA +:10A70000FFF7F0F806462046FFF76EF83F2802D91B +:10A710002846FFF7ABFF304670BD00BFACB5002048 +:10A7200037B5144C40F6B80200212046F9F734FE44 +:10A73000FF234FF4424201256371E2800023082287 +:10A7400063812273009304F138012B464FF4806239 +:10A7500004F110002581FFF731F800952B464FF4E6 +:10A76000806204F5876104F12400FFF727F803B045 +:10A7700030BD00BFACB5002010B50A4C0021052249 +:10A780002046F9F709FE04F110002434FFF7B0F871 +:10A790002046FFF7ADF820460121BDE81040FFF745 +:10A7A000B3B800BFACB50020F7B54B79022B064615 +:10A7B00003D00025284603B0F0BD8B79022BF8D1D9 +:10A7C000204FBB787BBB8B783B700C7809250C4401 +:10A7D00003E023781D44ADB21C446378242B1BD1C5 +:10A7E0009542F6D96378042B12D163790A2B0FD1E5 +:10A7F000154B277801930133009302231A46E11980 +:10A800003046FFF71DFA70B10E3517FA85F5ADB277 +:10A810000C48FFF7E9FECDE7052BE3D12146304692 +:10A82000FFF7EEF938B94FF0E023D3F8F03DDB073E +:10A83000BFD500BEBDE7A3787B7023781D44ADB2C1 +:10A840001C44CFE7ACB50020AEB5002070B50B4678 +:10A850001146127802F06002202A45D1234E8A88E0 +:10A860003478944240D14A78203A032A3CD8DFE831 +:10A8700002F00213162F2BB91D4A0723FFF702FE21 +:10A88000012070BD022BFBD11A4B002BF8D01849C8 +:10A890000020AFF30080F3E7002BF1D1ECE713B910 +:10A8A000FFF7DEFDECE7022BEAD14C881248347149 +:10A8B00004F0010585F00101FFF726F80F4B002B8E +:10A8C000DED0C4F3400229460020AFF30080D7E772 +:10A8D000002BE5D0022BD3D1094B002BD0D04988D7 +:10A8E0000020AFF30080CBE70020CAE7ACB5002022 +:10A8F000B2B5002000000000D0B50020000000002C +:10A90000000000002DE9F347374D1C46EB788B42E1 +:10A9100007460E4607D0AB788B4258D1AB78B3428E +:10A9200032D001245CE0A2B205F6380105F1100036 +:10A93000FEF7D8FF2D4B2BB92D4BEBB92A48FFF76B +:10A9400053FEEBE76B79FF2BF6D005F638094FF095 +:10A95000000805F1100AA045EED019F8013B6A790C +:10A960009A4206D15046FEF751FF10B96979AFF30C +:10A97000008008F10108EEE71E48FEF747FF0028B7 +:10A98000DCD1FDF789F9D9E71B4B13B10020AFF3F8 +:10A9900000800020FFF76AFE0028C2D11748FEF7AA +:10A9A00023FF0028BDD1002CBBD014F03F03B8D149 +:10A9B000A97801933846FFF779F9019B04460028EE +:10A9C000AFD0A9781A463846FFF7A4F908E04FF04F +:10A9D000E023D3F8F04D14F0010401D000BE0024B0 +:10A9E000204602B0BDE8F087ACB5002000000000B2 +:10A9F000997C0F00BCB5002000000000D0B50020FD +:10AA000030B4104B02249A6B83F82C10996883F8A9 +:10AA1000304093F83C408A1A9A6224B942F2050504 +:10AA20009D8783F83E4051B14AB11A7BD20930BCB0 +:10AA300014BF93F82E1093F82F10FFF7A9B930BC6C +:10AA4000704700BF64CE002038B5154B154C054645 +:10AA500073B1607BAFF3008050B942F2077384F8A2 +:10AA60003E00A38728460121BDE83840FFF7C8BF54 +:10AA7000A26BA36894F82F109B1AB3F5805F28BFD0 +:10AA80004FF48053084A9BB22846FFF743F930B988 +:10AA90004FF0E023D3F8F03DDB0700D500BE38BD12 +:10AAA0000000000064CE002064BE002073B5234C7B +:10AAB000E28AA36852BA92B2054612B1B3FBF2F22F +:10AAC00092B2A06BD4F81110B0FBF2F61B1AB3F5DA +:10AAD000805F28BF4FF4805309BA009302FB16022F +:10AAE000174B607B3144FDF7DBFB031E0CDA43F2AE +:10AAF0000333A38701210023284684F83E3002B0A7 +:10AB0000BDE87040FFF77CBF94F82E1006D100938B +:10AB10001A462846FFF751F802B070BD084A9BB2AA +:10AB20002846FFF7F7F80028F6D14FF0E023D3F8D6 +:10AB3000F03DDB07F0D500BEEEE700BF64CE00209D +:10AB400064BE0020C28A836852BA92B223B9002A36 +:10AB50000CBF002002207047C17B282904D1017B53 +:10AB6000C90906D1022070472A2902D1017BC909EF +:10AB7000F8D122B1934234BF022000207047012057 +:10AB800070470000044880F83C1080F83D2080F8B1 +:10AB90003E300120704700BF64CE002002484022B2 +:10ABA0000021F9F7F9BB00BF64CE00200248402223 +:10ABB0000021F9F7F1BB00BF64CE002073B54B79DB +:10ABC000082B054602D0002002B070BD8B79062B01 +:10ABD000F9D1CB79502BF6D1162A07D84FF0E023C4 +:10ABE000D3F8F03DD907EED500BEECE7164C8B78D4 +:10ABF00084F82D3004F12E030E78019304F12F0315 +:10AC0000009302231A463144FFF71AF838B94FF07F +:10AC1000E023D3F8F03DDA07D5D500BED4E7002312 +:10AC200084F8303094F82F101F2322462846FFF76F +:10AC300071F830B94FF0E023D3F8F03DDB0700D5D1 +:10AC400000BE1720C0E700BF64CE00207FB50646D7 +:10AC50001546A1B9137803F07F02022A48D16C7817 +:10AC6000012C45D16A88002A42D1AB883C4D95F829 +:10AC70003020042ADBB204D11946FFF789F80120FD +:10AC80001AE095F82E10994218D1022AF7D1AA6B32 +:10AC9000AB689B1AAB62032385F8303005F12002C4 +:10ACA0000D23FFF737F80028E9D14FF0E023D3F860 +:10ACB000F03DDB0752D500BE04B070BD95F82F10F3 +:10ACC0009942DCD1002ADAD10191FFF753F80199BA +:10ACD0000028D4D13046FFF78FF80028CFD10023C9 +:10ACE00085F830301E4A95F82F101F233046D8E7DC +:10ACF00003F06003202B31D16B78FE2B13D0FF2B98 +:10AD00002CD16B8853BBE9888AB23ABB144B3046CE +:10AD100099872946C3E90D2283F8302083F83E2025 +:10AD2000FFF79EFBABE76B88C3B9EB88012B15D10E +:10AD30008DF80F300B4B1BB1AFF300808DF80F0077 +:10AD40009DF80F3053B1013B8DF80F300DF10F021C +:10AD5000012329463046FFF795FB90E70020ABE73B +:10AD600064CE0020000000002DE9F041AB4C94F8C7 +:10AD70003070012F90B005461E4600F0A181032FD0 +:10AD800000F00D82002F41D194F82F308B4201D07A +:10AD9000012013E01F2E03D12268A14B9A4210D04C +:10ADA00094F82E100423284684F83030FEF7F0FF84 +:10ADB00094F82F102846FEF7EBFF002010B0BDE8F6 +:10ADC000F081984B23626368E67BD4F8088084F8AE +:10ADD0002C70C4E90937012384F8303006F0FD03F4 +:10ADE000282BC4E90D872BD12046FFF7ABFE014687 +:10ADF00018B12846FFF704FE08E0B8F1000F56D05E +:10AE0000282E40F0A8812846FFF750FE94F83030F5 +:10AE1000022BBDD194F82E102846FEF7EDFF002836 +:10AE2000B6D1A368A26B94F82E10934240F2E08151 +:10AE3000207BC00900F0DC812846FEF7A9FFA7E7C8 +:10AE4000B8F1000F17D0237BDB0914D1B8F5805F70 +:10AE500001D90121CDE7744A1FFA88F32846FEF78D +:10AE600059FF0028D2D14FF0E023D3F8F03DDA07A4 +:10AE7000A3D500BEA2E7252E677B08D8192E1AD8C5 +:10AE8000032E00F0F780122E00F09F8096B394F806 +:10AE90003C30002BDDD1A38E634A6449607BFDF713 +:10AEA000EDF9031ED5DB60D1A368002BD1D10223BD +:10AEB00084F83030AAE71A3E0B2EE8D801A353F8E5 +:10AEC00026F000BF35B00F0015AF0F008FAE0F009A +:10AED0008FAE0F008FAE0F008FAE0F008FAE0F0042 +:10AEE0008FAE0F008FAE0F0085AF0F008FAE0F003B +:10AEF0002FAF0F003846FDF7BFF90028D4D194F8E2 +:10AF00003C30002BC3D140F20243A387002384F8D6 +:10AF10003E30BCE7464B002BC6D0E17C3846C1F33F +:10AF2000400301F001020909FDF74EFAE5E70DF1D2 +:10AF3000160206A93846FDF73FFA069B13B1BDF885 +:10AF400016203AB994F83C30002BA0D140F20242CE +:10AF5000A287DCE712BA013B1BBA0892324807937A +:10AF6000082207A900F002FA0823A06800283FF48D +:10AF700070AF834228BF034663632B4A94F82E10B8 +:10AF80009BB26BE70023CDE90733099308238DF8C3 +:10AF90001F300DF11602022306A938468DF8243021 +:10AFA000FDF70AFA069A002ACCD0BDF81630002B1D +:10AFB000C8D012BA5BBA08921B48ADF826300C22F2 +:10AFC00007A900F0D3F90C23CFE72422002107A81A +:10AFD000F9F7E2F980238DF81D30082202232021A1 +:10AFE00009A88DF81E308DF81F30F9F7D5F9102219 +:10AFF00020210BA8F9F7D0F9042220210FA8F9F796 +:10B00000CBF90FAB0BAA09A93846FDF701F90648A1 +:10B01000242207A900F0AAF92423A6E764CE002081 +:10B02000555342435553425364BE002073CE002013 +:10B03000C9830F0003238DF81C3000238DF81D30C9 +:10B040008DF81E308DF81F30704B8BB13846AFF342 +:10B0500000809DF81E3080F0010060F3C7130422C9 +:10B060006B488DF81E3007A900F080F904237CE7B7 +:10B070000120EEE71222002107A8F9F78DF9F0234D +:10B0800094F83C208DF81C300A238DF823304FF0C3 +:10B09000000362F303038DF81E3094F83D308DF801 +:10B0A00028305B4894F83E308DF82930122207A9E9 +:10B0B00000F05CF90023A38784F83E30122354E7A4 +:10B0C000E37BA06B282B06D1636B30449842A063CE +:10B0D000BFF4EDAE97E62A2B41D1E28A52BA92B282 +:10B0E0001AB1A368B3FBF2F292B2D4F81110484F30 +:10B0F000B0FBF2FC09BA02FB1C020096607B3B46E7 +:10B100006144FDF7EFF8002809DAA36B3344A36329 +:10B1100043F20333A387002384F83E3099E6864246 +:10B1200012D9321A40B1A36B03920344391838463E +:10B13000A36300F0B5F9039A94F82F10002300934D +:10B140002846FEF73AFD61E6A36B1E44636BA663D7 +:10B150009E42BFF4ACAE2846FFF776FC56E6237B52 +:10B160003044DB09A0630CD1A38E294A607B04F133 +:10B170000F01FDF783F8002803DA39462846FFF768 +:10B180003FFCD4E90D329A42BFF491AE4FF0E02378 +:10B19000D3F8F03DDB077FF539AE00BE36E694F814 +:10B1A0002E308B427FF432AE0D2E7FF42FAEE37B38 +:10B1B000282B09D02A2B14D0164B53B1607B04F1F5 +:10B1C0000F01AFF3008004E0134B13B1607BAFF3CA +:10B1D0000080002384F83030104A94F82F101F2389 +:10B1E0003CE60F4B002BF4D0607BFDF793F8F0E7C3 +:10B1F0009B1AA362032384F830300A4A0D232846A1 +:10B20000FEF788FD00287FF4C3AD2CE600000000A7 +:10B2100064BE0020000000000000000064CE00209A +:10B2200015830F0084CE002008B50020FEF700FC37 +:10B2300030B94FF0E023D3F8F03DDB0700D500BE76 +:10B2400008BDFEF7EFBB8388C07800F0030002283A +:10B25000C3F30A0315D003281DD001280FD10229FA +:10B2600040F2FF3208BF4FF480629A420FD24FF093 +:10B27000E023D3F8F00D10F0010008D000BE00204C +:10B280007047022904D1B3F5007FF0D10120704747 +:10B29000402BFBD9EBE702290CBF4FF48062402220 +:10B2A0009A42F3D2E3E730B50A44914200D330BD6D +:10B2B0004C78052C06D18C7804F07F0500EB450511 +:10B2C000E4092B550C782144EFE700000649074AB2 +:10B2D000074B9B1A03DD043BC858D050FBDCF9F741 +:10B2E00063FEF8F7D5FF000068BD0F000080002066 +:10B2F000C8860020FEE7FEE7FEE7FEE7FEE7FEE782 +:10B30000FEE7FEE7FEE7FEE7032A70B515D940EA3F +:10B31000010C1CF0030F04460B4621D119462046B0 +:10B320000E680568B54204F1040403F1040317D163 +:10B33000043A032A20461946F0D8541EA2B100F15F +:10B34000FF3C013901E0C3180CD01CF801EF11F8E3 +:10B35000012F9645A4EB0C03F5D0AEEB020070BDB7 +:10B36000541EECE7184670BD104670BD844641EA95 +:10B37000000313F003036DD1403A41D351F8043B6D +:10B3800040F8043B51F8043B40F8043B51F8043BBF +:10B3900040F8043B51F8043B40F8043B51F8043BAF +:10B3A00040F8043B51F8043B40F8043B51F8043B9F +:10B3B00040F8043B51F8043B40F8043B51F8043B8F +:10B3C00040F8043B51F8043B40F8043B51F8043B7F +:10B3D00040F8043B51F8043B40F8043B51F8043B6F +:10B3E00040F8043B51F8043B40F8043B51F8043B5F +:10B3F00040F8043B51F8043B40F8043B403ABDD2CE +:10B40000303211D351F8043B40F8043B51F8043B6F +:10B4100040F8043B51F8043B40F8043B51F8043B2E +:10B4200040F8043B103AEDD20C3205D351F8043BFE +:10B4300040F8043B043AF9D2043208D0D2071CBFCA +:10B4400011F8013B00F8013B01D30B8803806046F3 +:10B45000704700BF082A13D38B078DD010F0030369 +:10B460008AD0C3F10403D21ADB071CBF11F8013BD9 +:10B4700000F8013B80D331F8023B20F8023B7BE728 +:10B48000043AD9D3013A11F8013B00F8013BF9D253 +:10B490000B7803704B7843708B78837060467047ED +:10B4A00088420DD98B1883420AD900EB020CBAB13D +:10B4B000624613F801CD02F801CD9942F9D17047E7 +:10B4C0000F2A0ED8034602F1FF3C4AB10CF1010CE1 +:10B4D000013B8C4411F8012B03F8012F6145F9D190 +:10B4E000704740EA01039B0750D1A2F1100370B5E9 +:10B4F00001F1200C23F00F0501F1100E00F11004F2 +:10B50000AC441B095EF8105C44F8105C5EF80C5CFF +:10B5100044F80C5C5EF8085C44F8085C5EF8045C77 +:10B5200044F8045C0EF1100EE64504F11004E9D174 +:10B53000013312F00C0F01EB031102F00F0400EBCA +:10B54000031327D0043C24F003064FEA940C1E4456 +:10B550001C1F8E465EF8045B44F8045FB442F9D1C8 +:10B560000CF1010402F0030203EB840301EB8401FC +:10B5700002F1FF3C4AB10CF1010C013B8C4411F883 +:10B58000012B03F8012F6145F9D170BD02F1FF3C99 +:10B5900003469BE72246EBE7830710B5044610D12C +:10B5A0000268A2F1013323EA020313F0803F08D1BD +:10B5B00050F8042FA2F1013323EA020313F0803F75 +:10B5C000F6D003781BB110F8013F002BFBD100F03F +:10B5D00003F8204610BD00BF80EA0102844612F045 +:10B5E000030F4FD111F0030F32D14DF8044D11F07C +:10B5F000040F51F8043B0BD0A3F101329A4312F02F +:10B60000803F04BF4CF8043B51F8043B16D100BF07 +:10B6100051F8044BA3F101329A4312F0803FA4F198 +:10B6200001320BD14CF8043BA24312F0803F04BF1F +:10B6300051F8043B4CF8044BEAD023460CF8013B8C +:10B6400013F0FF0F4FEA3323F8D15DF8044B704736 +:10B6500011F0010F06D011F8012B0CF8012B002A74 +:10B6600008BF704711F0020FBFD031F8022B12F063 +:10B67000FF0F16BF2CF8022B8CF8002012F47F4F1E +:10B68000B3D1704711F8012B0CF8012B002AF9D126 +:10B69000704700BF00000000000000000000000034 +:10B6A000000000000000000000000000000000009A +:10B6B000000000000000000000000000000000008A +:10B6C00090F800F06DE9024520F007016FF0000CE2 +:10B6D00010F0070491F820F040F049804FF000048A +:10B6E0006FF00700D1E9002391F840F000F1080065 +:10B6F00082FA4CF2A4FA8CF283FA4CF3A2FA8CF39D +:10B700004BBBD1E9022382FA4CF200F10800A4FA03 +:10B710008CF283FA4CF3A2FA8CF3E3B9D1E9042357 +:10B7200082FA4CF200F10800A4FA8CF283FA4CF38E +:10B73000A2FA8CF37BB9D1E9062301F1200182FA48 +:10B740004CF200F10800A4FA8CF283FA4CF3A2FA4E +:10B750008CF3002BC6D0002A04BF04301A4612BA5C +:10B76000B2FA82F2FDE8024500EBD2007047D1E95F +:10B77000002304F00305C4F100004FEAC50514F0EE +:10B78000040F91F840F00CFA05F562EA05021CBFBF +:10B7900063EA050362464FF00004A9E7F0B5254FC0 +:10B7A000A2F1020E164605460C460FCF8BB0EC46B2 +:10B7B000ACE80F000FCFACE80F0097E803004CF89F +:10B7C000040BBEF1220F8CF800102ED804F1FF3EBE +:10B7D00070464FF0000CB5FBF6F206FB125328330F +:10B7E0006B44614613F828CC00F801CF2B469E42EB +:10B7F00001F1010C1546EED9002304F80C3089B193 +:10B80000A44472461EF8010F1CF8015D8EF800502A +:10B810006FEA0E0302322344121B0B449A428CF847 +:10B820000000EEDB20460BB0F0BD002020700BB016 +:10B83000F0BD00BF34BD0F00FFF7B0BF53B94AB928 +:10B84000002908BF00281CBF4FF0FF314FF0FF3028 +:10B8500000F074B9ADF1080C6DE904CE00F006F803 +:10B86000DDF804E0DDE9022304B070472DE9F0477C +:10B87000089D04468E46002B4DD18A42944669D9D4 +:10B88000B2FA82F252B101FA02F3C2F1200120FAB7 +:10B8900001F10CFA02FC41EA030E94404FEA1C4805 +:10B8A000210CBEFBF8F61FFA8CF708FB16E341EA01 +:10B8B000034306FB07F199420AD91CEB030306F187 +:10B8C000FF3080F01F81994240F21C81023E6344A8 +:10B8D0005B1AA4B2B3FBF8F008FB103344EA03444C +:10B8E00000FB07F7A7420AD91CEB040400F1FF3361 +:10B8F00080F00A81A74240F207816444023840EA9E +:10B900000640E41B00261DB1D4400023C5E90043D6 +:10B910003146BDE8F0878B4209D9002D00F0EF8059 +:10B920000026C5E9000130463146BDE8F087B3FA8C +:10B9300083F6002E4AD18B4202D3824200F2F98074 +:10B94000841A61EB030301209E46002DE0D0C5E977 +:10B95000004EDDE702B9FFDEB2FA82F2002A40F0C3 +:10B960009280A1EB0C014FEA1C471FFA8CFE0126C6 +:10B97000200CB1FBF7F307FB131140EA01410EFB6A +:10B9800003F0884208D91CEB010103F1FF3802D211 +:10B99000884200F2CB804346091AA4B2B1FBF7F00B +:10B9A00007FB101144EA01440EFB00FEA64508D92E +:10B9B0001CEB040400F1FF3102D2A64500F2BB806B +:10B9C0000846A4EB0E0440EA03409CE7C6F12007BA +:10B9D000B34022FA07FC4CEA030C20FA07F401FA00 +:10B9E00006F31C43F9404FEA1C4900FA06F3B1FB89 +:10B9F000F9F8200C1FFA8CFE09FB181140EA0141EE +:10BA000008FB0EF0884202FA06F20BD91CEB01018A +:10BA100008F1FF3A80F08880884240F28580A8F1E2 +:10BA200002086144091AA4B2B1FBF9F009FB101134 +:10BA300044EA014100FB0EFE8E4508D91CEB0101D2 +:10BA400000F1FF346CD28E456AD90238614440EA75 +:10BA50000840A0FB0294A1EB0E01A142C846A646F5 +:10BA600056D353D05DB1B3EB080261EB0E0101FA7E +:10BA700007F722FA06F3F1401F43C5E900710026DB +:10BA80003146BDE8F087C2F12003D8400CFA02FC31 +:10BA900021FA03F3914001434FEA1C471FFA8CFE41 +:10BAA000B3FBF7F007FB10360B0C43EA064300FB31 +:10BAB0000EF69E4204FA02F408D91CEB030300F1CF +:10BAC000FF382FD29E422DD9023863449B1B89B286 +:10BAD000B3FBF7F607FB163341EA034106FB0EF30F +:10BAE0008B4208D91CEB010106F1FF3816D28B42BC +:10BAF00014D9023E6144C91A46EA004638E72E4688 +:10BB0000284605E70646E3E61846F8E64B45A9D27F +:10BB1000B9EB020864EB0C0E0138A3E74646EAE7EE +:10BB2000204694E74046D1E7D0467BE7023B61449C +:10BB300032E7304609E76444023842E7704700BF05 +:10BB4000F8B500BFF8BC08BC9E467047F8B500BF0A +:10BB5000F8BC08BC9E467047088000200010020018 +:10BB60000338FDD87047000000000000000000000E +:10BB70000338FDD87047010000000000089900203C +:10BB80000338FDD87047416461444655004D6573E4 +:10BB90006874617374696300302E392E32207331FA +:10BBA000343020362E312E3100000000000000001D +:10BBB00000000000000000000023D1BCEA5F7823F1 +:10BBC00015DEEF12120000000000000070B000202F +:10BBD0004164616672756974006E52462055463242 +:10BBE00000303132333435363738394142434445F9 +:10BBF00046006E52462053657269616C0009045319 +:10BC00006F66744465766963653A200053002E00C0 +:10BC10006E6F7420666F756E640D0A00EB3C905574 +:10BC20004632205546322000020101000240000049 +:10BC300000F80201010001000000000009010100FC +:10BC4000800029420042004D455348544153544915 +:10BC5000430046415431362020203C21646F6374F8 +:10BC60007970652068746D6C3E0A3C68746D6C3E3A +:10BC70003C626F64793E3C7363726970743E0A6C17 +:10BC80006F636174696F6E2E7265706C6163652895 +:10BC90002268747470733A2F2F6275796D656163D1 +:10BCA0006F666665652E636F6D2F6D61726B2E62B8 +:10BCB0006972737322293B0A3C2F73637269707433 +:10BCC0003E3C2F626F64793E3C2F68746D6C3E0A77 +:10BCD00000000000494E464F5F554632545854000C +:10BCE00024850020494E44455820202048544D00CA +:10BCF0005ABC0F0043555252454E5420554632000F +:10BD00000000000021A70F0079A70F00A9A70F00CE +:10BD10004DA80F0005A90F00000000009DAB0F000B +:10BD2000ADAB0F00BDAB0F004DAC0F0069AD0F0008 +:10BD30000000000030313233343536373839616233 +:10BD4000636465666768696A6B6C6D6E6F7071724B +:10BD5000737475767778797A00000000000000002F +:10BD60002885FF7F010000000880002000000000FF +:10BD700000000000F48200205C830020C4830020C7 +:10BD800000000000000000000000000000000000B3 +:10BD900000000000000000000000000000000000A3 +:10BDA0000000000000000000000000000000000093 +:10BDB0000000000000000000000000000000000083 +:10BDC0000000000000000000000000000000000073 +:10BDD0000000000000000000000000000000000063 +:10BDE0000000000000000000000000000000000053 +:10BDF0000000000000000000000000000000000043 +:10BE00000000000000000000000000000000000032 +:10BE10000000000000000000010000000000000021 +:10BE20000E33CDAB34126DE6ECDE05000B000000E6 +:10BE30000000000000000000000000000000000002 +:10BE400000000000000000000000000000000000F2 +:10BE500000000000000000000000000000000000E2 +:10BE600000000000000000000000000000000000D2 +:10BE700000000000000000000000000000000000C2 +:10BE800000000000000000000000000000000000B2 +:10BE900000000000000000000000000000000000A2 +:10BEA0000000000000000000000000000000000092 +:10BEB0000000000000000000000000000000000082 +:10BEC0000000000000000000000000000000000072 +:10BED0000000000000000000000000000000000062 +:10BEE0000000000000000000000000000000000052 +:10BEF0000000000000000000000000000000000042 +:10BF00000000000000000000000000000000000031 +:10BF10000000000000000000000000000000000021 +:10BF20000000000000000000000000000000000011 +:10BF30000000000000000000000000000000000001 +:10BF400000000000000000000000000000000000F1 +:10BF500000000000000000000000000000000000E1 +:10BF600000000000000000000000000000000000D1 +:10BF700000000000000000000000000000000000C1 +:10BF800000000000000000000000000000000000B1 +:10BF900000000000000000000000000000000000A1 +:10BFA0000000000000000000000000000000000091 +:10BFB0000000000000000000000000000000000081 +:10BFC0000000000000000000000000000000000071 +:10BFD0000000000000000000000000000000000061 +:10BFE0000000000000000000000000000000000051 +:10BFF0000000000000000000000000000000000041 +:10C000000000000000000000000000000000000030 +:10C010000000000000000000000000000000000020 +:10C020000000000000000000000000000000000010 +:10C030000000000000000000000000000000000000 +:10C0400000000000000000000000000000000000F0 +:10C0500000000000000000000000000000000000E0 +:10C0600000000000000000000000000000000000D0 +:10C0700000000000000000000000000000000000C0 +:10C0800000000000000000000000000000000000B0 +:10C0900000000000000000000000000000000000A0 +:10C0A0000000000000000000000000000000000090 +:10C0B0000000000000000000000000000000000080 +:10C0C0000000000000000000000000000000000070 +:10C0D0000000000000000000000000000000000060 +:10C0E0000000000000000000000000000000000050 +:10C0F0000000000000000000000000000000000040 +:10C10000000000000000000000000000000000002F +:10C11000000000000000000000000000000000001F +:10C12000000000000000000000000000000000000F +:10C1300000000000000000000000000000000000FF +:10C1400000000000000000000000000000000000EF +:10C1500000000000000000000000000000000000DF +:10C1600000000000000000000000000000000000CF +:10C1700000000000000000000000000000000000BF +:10C1800000000000000000000000000000000000AF +:10C190000000000000000000FFFFFFFF7C7F002088 +:10C1A0000090D003FF00FFFF320000007D7B0F00F6 +:10C1B000F17B0F0001090262000301008032080BCD +:10C1C000000202020000090400000102020004054E +:10C1D0002400200105240100010424020205240694 +:10C1E00000010705810308001009040100020A008C +:10C1F000000007050202400000070582024000001F +:10C200000904020002080650050705030240000069 +:10C210000705830240000009024B00020100803242 +:10C22000080B0002020200000904000001020200E3 +:10C230000405240020010524010001042402020554 +:10C24000240600010705810308001009040100020B +:10C250000A000000070502024000000705820240B4 +:10C26000000012010002EF0201409A2329000001A0 +:10C2700001020301FDBB0F008DBB0F008DBB0F0042 +:10C2800064B30020F2BB0F00D9BB0F00554632202B +:10C29000426F6F746C6F6164657220302E392E327C +:10C2A000206C69622F6E726678202876322E302ECE +:10C2B0003029206C69622F74696E79757362202849 +:10C2C000302E31322E302D3134352D673937373518 +:10C2D000653736393129206C69622F75663220281E +:10C2E00072656D6F7465732F6F726967696E2F6306 +:10C2F0006F6E6669677570646174652D392D67614D +:10C30000646262386337290D0A4D6F64656C3A20A8 +:10C3100068747470733A2F2F6275796D6561636FFD +:10C32000666665652E636F6D2F6D61726B2E626937 +:10C330007273730D0A426F6172642D49443A204D45 +:10C340006573687461737469630D0A446174653A56 +:10C350002053657020203120323032340D0A000025 +:10C3600000000000000000000000000000000000CD +:10C3700000000000000000000000000000000000BD +:10C3800000000000000000000000000000000000AD +:10C39000000000000000000000000000000000009D +:10C3A000000000000000000000000000000000008D +:10C3B000000000000000000000000000000000007D +:10C3C000000000000000000000000000000000006D +:10C3D000000000000000000000000000000000005D +:10C3E000000000000000000000000000000000004D +:10C3F000000000000000000000000000000000003D +:10C40000000000000000000000000000010000002B +:10C4100098B4002010000C000000E0FF1F00000096 +:10C42000000000005D450F0069420F0041420F000F +:10D80000F1109E1E797A22200500000064000000BD +:10D81000CC00000000001000CD000000000004005B +:10D82000D000000029009A23D10000004028A5ADB7 +:10D83000D2000000200000000000000000000000F6 +:10D8400000000000000000000000000000000000D8 +:08D850000000000000000000D0 +:020000041000EA +:0810140000400F0000E00F0096 +:00000001FF diff --git a/bin/generic/Meshtastic_6.1.0_bootloader-0.9.2_s140_6.1.1.zip b/bin/generic/Meshtastic_6.1.0_bootloader-0.9.2_s140_6.1.1.zip new file mode 100644 index 0000000000000000000000000000000000000000..ae035898dfa0e679bcfb1726c0f63075ec084f08 GIT binary patch literal 190874 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 literal 0 HcmV?d00001 diff --git a/bin/generic/Meshtastic_7.3.0_bootloader-0.9.2_s140_7.3.0.hex b/bin/generic/Meshtastic_7.3.0_bootloader-0.9.2_s140_7.3.0.hex new file mode 100644 index 0000000..37750fc --- /dev/null +++ b/bin/generic/Meshtastic_7.3.0_bootloader-0.9.2_s140_7.3.0.hex @@ -0,0 +1,11851 @@ +:04000003F000B2CD8A +:020000040000FA +:1000000000040020810A000015070000610A0000BA +:100010001F07000029070000330700000000000050 +:10002000000000000000000000000000A50A000021 +:100030003D070000000000004707000051070000D6 +:100040005B070000650700006F07000079070000EC +:10005000830700008D07000097070000A10700003C +:10006000AB070000B5070000BF070000C90700008C +:10007000D3070000DD070000E7070000F1070000DC +:10008000FB070000050800000F0800001908000029 +:10009000230800002D080000370800004108000078 +:1000A0004B080000550800005F08000069080000C8 +:1000B000730800007D080000870800009108000018 +:1000C0009B080000A5080000AF080000B908000068 +:1000D000C3080000CD080000D7080000E1080000B8 +:1000E000EB080000F5080000FF0800000909000007 +:1000F000130900001D090000270900003109000054 +:100100003B0900001FB500F003F88DE80F001FBD8C +:1001100000F0ACBC40F6FC7108684FF01022401CA7 +:1001200008D00868401C09D00868401C04D0086842 +:1001300000F037BA9069F5E79069F9E7704770B554 +:100140000B46010B184400F6FF70040B4FF0805073 +:100150000022090303692403406943431D1B104621 +:1001600000F048FA29462046BDE8704000F042BA47 +:10017000F0B54FF6FF734FF4B4751A466E1E11E0DA +:10018000A94201D3344600E00C46091B30F8027B3B +:10019000641E3B441A44F9D19CB204EB134394B25D +:1001A00004EB12420029EBD198B200EB134002EBB2 +:1001B000124140EA0140F0BDF34992B00446D1E952 +:1001C0000001CDE91001FF224021684600F0F4FB58 +:1001D00094E80F008DE80F00684610A902E004C8FB +:1001E00041F8042D8842FAD110216846FFF7C0FF7C +:1001F0001090AA208DF8440000F099F9FFF78AFFCB +:1002000040F6FC7420684FF01025401C0FD0206889 +:1002100010226946803000F078F92068401C08D030 +:100220002068082210A900F070F900F061F9A869AF +:10023000EEE7A869F5E74FF080500369406940F6A2 +:10024000FC71434308684FF01022401C06D0086838 +:1002500000F58050834203D2092070479069F7E788 +:100260000868401C04D00868401C03D00020704778 +:100270009069F9E70420704770B504460068C34DE3 +:10028000072876D2DFE800F033041929631E250021 +:10029000D4E9026564682946304600F062F92A46CE +:1002A0002146304600F031F9AA002146304600F0E0 +:1002B00057FB002800D0032070BD00F009FC4FF46C +:1002C000805007E0201D00F040F90028F4D100F034 +:1002D000FFFB60682860002070BD241D94E80700C3 +:1002E000920000F03DFB0028F6D00E2070BDFFF715 +:1002F000A2FF0028FAD1D4E901034FF0805100EBAE +:10030000830208694D69684382420ED840F6F8704E +:1003100005684FF010226D1C09D0056805EB8305B8 +:100320000B6949694B439D4203D9092070BD55694A +:10033000F4E70168491C03D00068401C02D003E0C8 +:100340005069FAE70F2070BD2046FFF735FFFFF731 +:1003500072FF0028F7D1201D00F0F7F80028F2D135 +:1003600060680028F0D100F0E2F8FFF7D3FE00F05B +:10037000BFF8072070BD10B50C46182802D0012028 +:10038000086010BD2068FFF777FF206010BD41684E +:10039000054609B1012700E0002740F6F8742068FF +:1003A0004FF01026401C2BD02068AA68920000F065 +:1003B000D7FA38B3A86881002068401C27D020688D +:1003C000FFF7BDFED7B12068401C22D026684FF051 +:1003D0008050AC686D68016942695143A9420DD9EA +:1003E000016940694143A14208D92146304600F0E5 +:1003F000B8F822462946304600F087F800F078F831 +:100400007069D2E700F093F8FFF784FEF6E77069B1 +:10041000D6E77669DBE740F6FC7420684FF01026DB +:10042000401C23D02068401C0CD02068401C1FD0EA +:100430002568206805F18005401C1BD027683879A5 +:10044000AA2819D040F6F8700168491C42D001680A +:10045000491C45D00168491C3ED001680968491C07 +:100460003ED00168491C39D000683EE0B069DAE747 +:10047000B569DEE7B769E2E710212846FFF778FEA5 +:100480003968814222D12068401C05D0D4F8001080 +:1004900001F18002C03107E0B169F9E730B108CA63 +:1004A00051F8040D984201D1012000E000208A4259 +:1004B000F4D158B1286810B1042803D0FEE72846CB +:1004C000FFF765FF3149686808600EE0FFF722FE1C +:1004D00000F00EF87169BBE77169BFE7706904E06D +:1004E0004FF480500168491C01D000F0CBFAFEE7C0 +:1004F000BFF34F8F26480168264A01F4E06111439B +:100500000160BFF34F8F00BFFDE72DE9F0411746B3 +:100510000D460646002406E03046296800F054F8EF +:10052000641C2D1D361DBC42F6D3BDE8F08140F69B +:10053000FC700168491C04D0D0F800004FF48051D1 +:10054000FDE54FF010208069F8E74FF080510A690F +:10055000496900684A43824201D810207047002050 +:10056000704770B50C4605464FF4806608E0284693 +:1005700000F017F8B44205D3A4F5806405F5805562 +:10058000002CF4D170BD0000F40A0000000000202F +:100590000CED00E00400FA05144801680029FCD0C5 +:1005A0007047134A0221116010490B68002BFCD0E0 +:1005B0000F4B1B1D186008680028FCD0002010603D +:1005C00008680028FCD07047094B10B501221A605A +:1005D000064A1468002CFCD0016010680028FCD08A +:1005E0000020186010680028FCD010BD00E4014015 +:1005F00004E5014070B50C46054600F073F810B9EB +:1006000000F07EF828B121462846BDE8704000F091 +:1006100007B821462846BDE8704000F037B8000012 +:100620007FB5002200920192029203920A0B000B06 +:100630006946012302440AE0440900F01F0651F80C +:10064000245003FA06F6354341F82450401C8242F8 +:10065000F2D80D490868009A10430860081D016827 +:10066000019A1143016000F03DF800280AD00649C4 +:1006700010310868029A10430860091D0868039A3F +:10068000104308607FBD00000006004030B50F4CED +:10069000002200BF04EB0213D3F800582DB9D3F8A1 +:1006A000045815B9D3F808581DB1521C082AF1D3C3 +:1006B00030BD082AFCD204EB0212C2F80008C3F8CD +:1006C00004180220C3F8080830BD000000E0014013 +:1006D0004FF08050D0F83001082801D0002070473A +:1006E000012070474FF08050D0F83011062905D016 +:1006F000D0F83001401C01D0002070470120704725 +:100700004FF08050D0F830010A2801D00020704707 +:100710000120704708208F490968095808471020B0 +:100720008C4909680958084714208A4909680958FA +:100730000847182087490968095808473020854923 +:100740000968095808473820824909680958084744 +:100750003C20804909680958084740207D490968BC +:100760000958084744207B49096809580847482028 +:1007700078490968095808474C207649096809589A +:10078000084750207349096809580847542071499F +:1007900009680958084758206E49096809580847E8 +:1007A0005C206C4909680958084760206949096854 +:1007B00009580847642067490968095808476820AC +:1007C00064490968095808476C2062490968095852 +:1007D000084770205F4909680958084774205D4937 +:1007E00009680958084778205A490968095808478C +:1007F0007C205849096809580847802055490968EC +:10080000095808478420534909680958084788202F +:1008100050490968095808478C204E490968095809 +:10082000084790204B4909680958084794204949CE +:10083000096809580847982046490968095808472F +:100840009C204449096809580847A0204149096883 +:1008500009580847A4203F49096809580847A820B3 +:100860003C49096809580847AC203A4909680958C1 +:100870000847B0203749096809580847B420354966 +:10088000096809580847B8203249096809580847D3 +:10089000BC203049096809580847C0202D4909681B +:1008A00009580847C4202B49096809580847C82037 +:1008B0002849096809580847CC2026490968095879 +:1008C0000847D0202349096809580847D4202149FE +:1008D000096809580847D8201E4909680958084777 +:1008E000DC201C49096809580847E02019490968B3 +:1008F00009580847E4201749096809580847E820BB +:100900001449096809580847EC2012490968095830 +:100910000847F0200F49096809580847F4200D4995 +:10092000096809580847F8200A490968095808471A +:10093000FC2008490968095808475FF48070054998 +:10094000096809580847000003480449024A034B54 +:100950007047000000000020000B0000000B0000AA +:1009600040EA010310B59B070FD1042A0DD310C82C +:1009700008C9121F9C42F8D020BA19BA884201D97E +:10098000012010BD4FF0FF3010BD1AB1D30703D0C6 +:10099000521C07E0002010BD10F8013B11F8014B7C +:1009A0001B1B07D110F8013B11F8014B1B1B01D198 +:1009B000921EF1D1184610BD02F0FF0343EA032254 +:1009C00042EA024200F005B87047704770474FF0A6 +:1009D00000020429C0F0128010F0030C00F01B800C +:1009E000CCF1040CBCF1020F18BF00F8012BA8BF1A +:1009F00020F8022BA1EB0C0100F00DB85FEAC17CDE +:100A000024BF00F8012B00F8012B48BF00F8012B90 +:100A100070474FF0000200B51346944696462039C1 +:100A200022BFA0E80C50A0E80C50B1F12001BFF4A7 +:100A3000F7AF090728BFA0E80C5048BF0CC05DF80D +:100A400004EB890028BF40F8042B08BF704748BF5B +:100A500020F8022B11F0804F18BF00F8012B7047CF +:100A6000014B1B68DB6818470000002009480A4951 +:100A70007047FFF7FBFFFFF745FB00BD20BFFDE719 +:100A8000064B1847064A1060016881F308884068E1 +:100A900000470000000B0000000B000017040000DE +:100AA000000000201EF0040F0CBFEFF30881EFF3ED +:100AB0000981886902380078182803D100E0000015 +:100AC000074A1047074A12682C3212681047000084 +:100AD00000B5054B1B68054A9B58984700BD0000B0 +:100AE0007703000000000020F00A0000040000006E +:100AF000001000000000000000FFFFFF0090D00386 +:10100000C8130020395E020085C100009F5D020008 +:1010100085C1000085C1000085C1000000000000FE +:10102000000000000000000000000000C55E02009B +:1010300085C100000000000085C1000085C10000DE +:101040002D5F0200335F020085C1000085C10000F2 +:1010500085C1000085C1000085C1000085C1000078 +:10106000395F020085C1000085C100003F5F0200BA +:1010700085C10000455F02004B5F0200515F020026 +:1010800085C1000085C1000085C1000085C1000048 +:1010900085C1000085C1000085C1000085C1000038 +:1010A00085C10000575F020085C1000085C10000B6 +:1010B00085C1000085C1000085C1000085C1000018 +:1010C0005D5F020085C1000085C1000085C1000090 +:1010D00085C1000085C1000085C1000085C10000F8 +:1010E00085C1000085C1000085C1000085C10000E8 +:1010F00085C1000085C1000085C1000085C10000D8 +:1011000085C1000085C1000000F002F824F083FED4 +:101110000AA090E8000C82448344AAF10107DA4552 +:1011200001D124F078FEAFF2090EBAE80F0013F0F7 +:10113000010F18BFFB1A43F00103184718530200B0 +:10114000385302000A444FF0000C10F8013B13F032 +:10115000070408BF10F8014B1D1108BF10F8015B10 +:10116000641E05D010F8016B641E01F8016BF9D103 +:1011700013F0080F1EBF10F8014BAD1C0C1B09D15A +:101180006D1E58BF01F801CBFAD505E014F8016BCC +:1011900001F8016B6D1EF9D59142D6D3704700005E +:1011A0000023002400250026103A28BF78C1FBD870 +:1011B000520728BF30C148BF0B6070471FB500F011 +:1011C00003F88DE80F001FBD24F022BE70B51A4C45 +:1011D00005460A202070A01C00F0D5F85920A080F8 +:1011E00029462046BDE8704008F082B908F08BB966 +:1011F00070B50C461149097829B1A0F160015E294A +:1012000008D3012013E0602804D0692802D043F2FB +:1012100001000CE020CC0A4E94E80E0006EB8000A2 +:10122000A0F58050241FD0F8806E2846B04720607B +:1012300070BD012070470000080000201C00002045 +:10124000A05F02003249884201D20120704700208D +:10125000704770B50446A0F500002E4EB0F1786FCF +:1012600002D23444A4F500042948844201D2012565 +:1012700000E0002500F043F848B125B9B44204D39A +:101280002548006808E0012070BD002070BD002DD9 +:10129000F9D1B442F9D321488442F6D2F3E710B52C +:1012A0000446A0F50000B0F1786F03D21948044459 +:1012B000A4F5000400F023F84FF0804130B1164847 +:1012C000006804E08C4204D2012003E01348844209 +:1012D000F8D2002080F0010010BD10B520B1FFF75A +:1012E000DEFF08B1012010BD002010BD10B520B1F7 +:1012F000FFF7AFFF08B1012010BD002010BD084866 +:1013000008490068884201D10120704700207047D9 +:1013100000700200000000202000002008000020D3 +:101320005C000020BEBAFECA10B5044600210120B0 +:1013300000F042F800210B2000F03EF800210820C8 +:1013400000F03AF80421192000F036F804210D20AD +:1013500000F032F804210E2000F02EF804210F20B6 +:1013600000F02AF80421C84300F026F806211620D0 +:1013700000F022F80621152000F01EF82046FFF7A5 +:1013800025FF002010BD40F2231101807047FFF7B8 +:101390002DBF1148704710487047104A10B51468A7 +:1013A0000E4B0F4A08331A60FFF722FF0B48001D4F +:1013B000046010BD704770474907090E002804DB20 +:1013C00000F1E02080F80014704700F00F0000F1F9 +:1013D000E02080F8141D704703F900421005024018 +:1013E00001000001FD48002101604160018170475A +:1013F0002DE9FF4F93B09B46209F160004460DD069 +:101400001046FFF726FF18B1102017B0BDE8F08F87 +:101410003146012001F0D3FE0028F6D101258DF8D8 +:1014200042504FF4C050ADF84000002210A92846A9 +:1014300006F0C5FC0028E8D18DF84250A8464FF4CC +:1014400028500025ADF840001C2229466846079523 +:101450000DF01DF89DF81C000DF11C0A20F00F0086 +:10146000401C20F0F00010308DF81C0020788DF822 +:101470001D0061789DF81E000DF1400961F34200E6 +:1014800040F001008DF81E009DF8000008AA40F011 +:1014900002008DF800002089ADF83000ADF8325020 +:1014A0006089ADF83400CDF82CA060680E900AA9D0 +:1014B000CDF82890684606F090FA0028A5D160681B +:1014C000FFF70BFF40B16068FFF710FF20B96078AD +:1014D00000F00300022801D0012000E00020BF4CF2 +:1014E00008AA0AA92072BDF8200020808DF8428049 +:1014F00042F60120ADF840009DF81E0020F00600E5 +:10150000801C20F001008DF81E000220ADF8300094 +:10151000ADF8340014A80E90684606F05EFA002874 +:1015200089D1BDF82000608036B1211D304600F021 +:101530005FF90028C2D109E0BBF1000F05D00CF023 +:1015400021FDE8BB0CF01EFDD0BBA58017B1012F1B +:1015500043D04AE08DF8428042F6A620ADF8400024 +:1015600046461C220021684607950CF090FF9DF826 +:101570001C00ADF8346020F00F00401C20F0F0009B +:1015800010308DF81C009DF81D0020F0FF008DF834 +:101590001D009DF81E0020F0060040F00100801C98 +:1015A0008DF81E009DF800008DF8446040F00200A8 +:1015B0008DF80000CDE90A9AADF8306011A800E07E +:1015C00011E00E9008AA0AA9684606F006FA00285B +:1015D000A6D1BDF82000E08008E00CF0D3FC10B9E3 +:1015E0000CF0D0FC08B103200FE7E58000200CE7E9 +:1015F0003EB50446794D0820ADF80000A88828B112 +:101600002046FFF726FE18B110203EBD06203EBD45 +:101610002146012001F0D3FD0028F8D12088ADF843 +:1016200004006088ADF80600A088ADF80800E088E6 +:10163000ADF80A00A88801AB6A46002106F0AAFDB1 +:10164000BDF800100829E2D003203EBD7FB5634DF0 +:101650000446A88868B1002002900820ADF8080070 +:10166000CDF80CD02046FFF7F4FD20B1102004B0D7 +:1016700070BD0620FBE7A98802AA4FF6FF7006F0AE +:10168000CCFF0028F3D1BDF80810082901D00320B1 +:10169000EDE7BDF800102180BDF802106180BDF8B3 +:1016A0000410A180BDF80610E180E0E701B582B02A +:1016B0000220ADF80000494802AB6A46408800218C +:1016C00006F068FDBDF80010022900D003200EBD11 +:1016D0001CB5002100910221ADF800100190FFF728 +:1016E000DEFD08B110201CBD3C486A4641884FF61B +:1016F000FF7006F092FFBDF800100229F3D003201E +:101700001CBDFEB5354C06461546207A0F46C0076F +:1017100005D00846FFF79DFD18B11020FEBD0F2033 +:10172000FEBDF82D01D90C20FEBD3046FFF791FD1E +:1017300018BB208801A905F03AFE0028F4D13078C2 +:101740008DF80500208801A906F003FD0028EBD1E3 +:1017500000909DF800009DF8051040F002008DF803 +:101760000000090703D040F008008DF80000208831 +:10177000694606F08BFC0028D6D1ADF808502088C9 +:101780003B4602AA002106F005FDBDF80810A9425B +:10179000CAD00320FEBD7CB5054600200090019014 +:1017A0000888ADF800000C4628460195FFF795FD26 +:1017B00018B92046FFF773FD08B110207CBD15B1A4 +:1017C000BDF8000060B105486A4601884FF6FF7019 +:1017D00006F023FFBDF8001021807CBD240200200C +:1017E0000C20FAE72F48C088002800D0012070475D +:1017F00030B5044693B000200D46014600901422F7 +:1018000001A80CF044FE1C22002108A80CF03FFEA9 +:101810009DF80000CDF808D020F00F00401C20F00B +:10182000F00010308DF800009DF8010006AA20F0AD +:10183000FF008DF801009DF8200001A940F0020092 +:101840008DF8200001208DF8460042F60420ADF806 +:10185000440011A801902088ADF83C006088ADF8E4 +:101860003E00A088ADF84000E088ADF842009DF849 +:10187000020020F00600801C20F001008DF802001C +:101880000820ADF80C00ADF810000FA8059008A8CE +:1018900006F0A3F8002803D1BDF818002880002026 +:1018A00013B030BD24020020F0B5007B059F1E461A +:1018B00014460D46012800D0FFDF0C2030803A206E +:1018C0003880002C08D0287A032806D0287B0128ED +:1018D00000D0FFDF17206081F0BDA889FBE72DE96C +:1018E000F0470D4686B095F80C900E991446B9F164 +:1018F000010F0BD01022007B2E8A9046052807D0BE +:10190000062839D0FFDF06B0BDE8F0870222F2E7F3 +:10191000E8890C2200EB400002EB400018803320E5 +:101920000880002CEFD0E8896081002720E0009635 +:10193000688808F1020301AA696900F097FF06EBC5 +:101940000800801C07EB470186B204EB4102BDF89A +:1019500004009081F848007808B1012300E00023DA +:101960000DF1060140460E3214F029F87F1CBFB27B +:101970006089B842DBD8C6E734200880E889B9F12D +:10198000010F11D0122148430E301880002CBAD01C +:10199000E88960814846B9F1010F00D00220207328 +:1019A00000270DF1040A1FE00621ECE70096688885 +:1019B00008F1020301AA696900F058FF06EB08006C +:1019C000801C86B2B9F1010F12D007EBC70004EBFF +:1019D0004000BDF80410C18110220AF1020110304C +:1019E0000CF02BFD7F1CBFB26089B842DED88AE7BD +:1019F00007EB470104EB4102BDF80400D0810AF176 +:101A000002014046103213F0FCFFEBE72DE9F047EE +:101A10000E4688B090F80CC096F80C80378AF5898D +:101A20000C20DFF81493109902F10C04BCF1030FA1 +:101A300008D0BCF1040F3DD0BCF1070F75D0FFDF1B +:101A400008B061E705EB850C00EB4C0018803120F5 +:101A50000880002AF4D0A8F1060000F0FF0A5581A2 +:101A600024E01622002101A80CF011FD00977088D7 +:101A7000434601AA716900F0F9FEBDF80400208018 +:101A8000BDF80600E080BDF80800208199F800004C +:101A900008B1012300E00023A21C0DF10A01504609 +:101AA00013F08DFF07EB080087B20A346D1EADB24C +:101AB000D7D2C5E705EB850C00EB4C00188032202F +:101AC0000880002ABCD0A8F1050000F0FF0A55816B +:101AD00037E000977088434601AA716900F0C6FE9E +:101AE0009DF80600BDF80410E1802179420860F3FA +:101AF000000162F34101820862F38201C20862F3CD +:101B0000C301020962F30411420962F3451182091B +:101B100062F386112171C0096071BDF80700208150 +:101B200099F8000010B1012301E00EE000232246E5 +:101B30000DF10901504613F042FF07EB080087B290 +:101B40000A346D1EADB2C4D27AE7A8F1020084B2A5 +:101B500005FB08FC0CF10E00188035200880002AD7 +:101B6000A7D05581948100971FFA8CF370880E32AC +:101B7000716900F07BFE63E72DE9F84F1E460A9D70 +:101B80000C4681462AB1607A00F58070D080E089E9 +:101B9000108199F80C000C274FF000084FF00E0A46 +:101BA0000D2872D2DFE800F09D070E1B272F374566 +:101BB000546972727200214648460095FFF774FE20 +:101BC000BDE8F88F207B9146082802D0032800D07A +:101BD000FFDF3780302009E0A9F80A80F0E7207B9A +:101BE0009146042800D0FFDF378031202880B9F1EA +:101BF000000FF1D1E4E7207B9146042800D0FFDFFD +:101C000037803220F2E7207B9146022800D0FFDFA8 +:101C100037803320EAE7207B1746022800D0FFDF19 +:101C20003420A6F800A02880002FC9D0A7F80A8089 +:101C3000C6E7207B1746042800D0FFDF3520A6F832 +:101C400000A02880002FBBD04046A7F80A8012E0F1 +:101C5000207B1746052802D0062800D0FFDF102081 +:101C6000308036202880002FAAD0E0897881A7F81C +:101C70000E80B9F80E00B881A2E7207B91460728B4 +:101C800000D0FFDF37803720B0E72AE04FF01200A6 +:101C900018804FF038001700288091D0E0897881B3 +:101CA000A7F80E80A7F8108099F80C000A2805D034 +:101CB0000B2809D00C280DD0FFDF81E7207B0A28F4 +:101CC00000D0FFDF01200AE0207B0B2800D0FFDFDF +:101CD000042004E0207B0C2800D0FFDF05203873AF +:101CE0006EE7FFDF6CE770B50C46054601F0AAFB16 +:101CF00020B10078222804D2082070BD43F20200EF +:101D000070BD0521284612F0D1F8206008B10020EE +:101D100070BD032070BD30B44880087820F00F00FB +:101D2000C01C20F0F000903001F8080B1DCA81E8BB +:101D30001D0030BC07F05DBC100000202DE9FF47FE +:101D400084B0002782460297079890468946123051 +:101D50000AF069FA401D20F00306079828B907A980 +:101D60005046FFF7C0FF002854D1B9F1000F05D04D +:101D70000798017B19BB052504681BE098F8000053 +:101D8000092803D00D2812D0FFDF46E0079903256C +:101D90004868B0B3497B42887143914239D98AB2CD +:101DA000B3B2011D11F0F5FE0446078002E0079C66 +:101DB000042508340CB1208810B1032D29D02CE063 +:101DC0000798012112300AF060FAADF80C000246C3 +:101DD00002AB2946504608F0B8FA070001D1A01C12 +:101DE000029007983A461230C8F80400A8F802A0FA +:101DF00003A94046029B0AF055FAD8B10A2817D227 +:101E000000E006E0DFE800F007091414100B0D14E1 +:101E10001412132014E6002012E6112010E6082008 +:101E20000EE643F203000BE6072009E60D2007E665 +:101E3000032005E6BDF80C002346CDE900702A46D4 +:101E40005046079900F022FD57B9032D08D1079895 +:101E5000B3B2417B406871438AB2011D11F0ADFEFF +:101E6000B9F1000FD7D0079981F80C90D3E72DE98D +:101E7000FE4F91461A881C468A468046FAB102AB4C +:101E8000494608F062FA050019D04046A61C27888A +:101E900012F04FF93246072629463B46009611F0CC +:101EA0005EFD20882346CDE900504A465146404613 +:101EB00000F0ECFC002020800120BDE8FE8F002017 +:101EC000FBE710B586B01C46AAB104238DF800309C +:101ED0001388ADF808305288ADF80A208A788DF85A +:101EE0000E200988ADF80C1000236A462146FFF742 +:101EF00025FF06B010BD1020FBE770B50D4605218B +:101F000011F0D4FF040000D1FFDF294604F11200D4 +:101F1000BDE870400AF0A2B92DE9F8430D468046AD +:101F2000002607F063FB04462878102878D2DFE803 +:101F300000F0773B345331311231313108313131D6 +:101F400031312879001FC0B2022801D0102810D1E9 +:101F500014BBFFDF35E004B9FFDF0521404611F077 +:101F6000A5FF007B032806D004280BD0072828D023 +:101F7000FFDF072655E02879801FC0B2022820D055 +:101F800050B1F6E72879401FC0B2022819D01028B6 +:101F900017D0EEE704B9FFDF13E004B9FFDF2879BB +:101FA00001280ED1172137E00521404611F07EFFB0 +:101FB000070000D1FFDF07F1120140460AF02BF9BC +:101FC0002CB12A4621464046FFF7A5FE29E0132101 +:101FD000404602F01FFD24E004B9FFDF0521404622 +:101FE00011F064FF060000D1FFDF694606F1120020 +:101FF0000AF01BF9060000D0FFDFA988172901D2DB +:10200000172200E00A46BDF80000824202D90146CC +:1020100002E005E01729C5D3404600F047FCD0E7B1 +:10202000FFDF3046BDE8F883401D20F0030219B100 +:1020300002FB01F0001D00E000201044704713B5C2 +:10204000009858B10024684611F04DFD002C04D1D1 +:10205000F749009A4A6000220A701CBD0124002042 +:10206000F2E72DE9F0470C461546242200212046D0 +:102070000CF00DFA05B9FFDFA87860732888DFF847 +:10208000B0A3401D20F00301AF788946DAF80400C0 +:1020900011F047FD060000D1FFDF4FF00008266079 +:1020A000A6F8008077B109FB07F1091D0AD0DAF81C +:1020B000040011F036FD060000D1FFDF6660C6F8AF +:1020C000008001E0C4F80480298804F11200BDE812 +:1020D000F0470AF091B82DE9F047804601F112006F +:1020E0000D4681460AF09FF8401DD14F20F00302B3 +:1020F0006E7B14462968786811F03EFD3EB104FB02 +:1021000006F2121D03D06968786811F035FD0520CC +:1021100011F074FE0446052011F078FE201A012803 +:1021200002D1786811F0F2FC49464046BDE8F0471C +:102130000AF078B870B50546052111F0B7FE040025 +:1021400000D1FFDF04F112012846BDE870400AF01B +:1021500062B82DE9F04F91B04FF0000BADF828B008 +:10216000ADF804B047880C4605469246052138462E +:1021700011F09CFE060000D1FFDF24B1A780A4F877 +:1021800006B0A4F808B0297809220B20B2EB111F81 +:1021900073D12A7A04F1100138274FF00C084FF060 +:1021A00012090291102A69D2DFE802F068F2F1F018 +:1021B0008008D3898EA03DDCF3EEB7B7307B0228D0 +:1021C00000D0FFDFA88908EBC001ADF80410302172 +:1021D000ADF82810002C25D06081B5F80E800027BE +:1021E0001DE004EBC709317C89F80E10F189A9F8CC +:1021F0000C10CDF800806888042305AA296900F036 +:1022000035FBBDF81410A9F8101008F10400BDF852 +:1022100016107F1C1FFA80F8A9F81210BFB260894F +:10222000B842DED80CE1307B022800D0FFDFE9891C +:1022300008EBC100ADF804003020ADF8280095F897 +:102240000C90002CA9F10400C0B20F90EAD061817B +:10225000B5F81080002725E0CDF8008068884B464F +:1022600003AA696900F002FB08EB09001FFA80F875 +:102270006F48007818B1012302E0DDE0DAE00023C6 +:1022800004EBC702009204A90C320F9813F097FBDD +:10229000009ABDF80C007F1C1082009ABDF80E0059 +:1022A000BFB250826089B842D6D8C9E00AA800906F +:1022B00001AB224629463046FFF711FBC0E0307BD8 +:1022C000082805D0FFDF03E0307B082800D0FFDFBF +:1022D000E8891030ADF804003620ADF82800002C55 +:1022E0003FD0A9896181F189A18127E0307B09284C +:1022F00000D0FFDFA88901460C30ADF8040037207C +:10230000ADF82800002C2CD06181E8890090AB89C1 +:10231000688804F10C02296955E0E88939211030F8 +:1023200080B2ADF80400ADF82810002C72D0A98955 +:102330006181287A0E280AD002212173E989E1817E +:10234000288A0090EB8968886969029A3BE001213C +:10235000F3E70AA8009001AB224629463046FFF772 +:1023600055FB6DE0307B0A2800D0FFDFADF804900C +:10237000ADF828704CB3A9896181A4F810B0A4F815 +:102380000EB0012020735BE020E002E030E038E096 +:1023900041E0307B0B2800D0FFDF288AADF82870A1 +:1023A0001230ADF8040084B104212173A989618140 +:1023B000E989E181298A2182688A00902B8A6888CC +:1023C00004F11202696900F051FA39E0307B0C28FF +:1023D00000D0FFDFADF80490ADF828703CB30521C4 +:1023E0002173A4F80AB0A4F80EB0A4F810B027E046 +:1023F0000AA8009001AB224629463046FFF754FA5E +:102400001EE00AA8009001AB224629463046FFF79D +:10241000B3FB15E034E03B21ADF80400ADF8281023 +:1024200074B30120E080A4F808B084F80AB007E093 +:1024300010000020FFDF03E0297A012917D0FFDF19 +:10244000BDF80400AAF800006CB1BDF82800208097 +:10245000BDF804006080BDF82800392803D03C286E +:1024600001D086F80CB011B00020BDE8F08F3C21FF +:10247000ADF80400ADF8281014B1697AA172DFE755 +:10248000AAF80000EFE72DE9F84356880F4680468A +:1024900015460521304611F009FD040000D1FFDF8B +:1024A000123400943B46414630466A680AF02EF8E2 +:1024B000B8E570B50D46052111F0F8FC040000D117 +:1024C000FFDF294604F11200BDE8704009F0B8BEF4 +:1024D00070B50D46052111F0E9FC040000D1FFDFC5 +:1024E000294604F11200BDE8704009F0D6BE70B56F +:1024F0000546052111F0DAFC040000D1FFDF04F1EC +:10250000080321462846BDE870400422AFE470B5B8 +:102510000546052111F0CAFC040000D1FFDF214669 +:1025200028462368BDE870400522A0E470B5064641 +:10253000052111F0BBFC040000D1FFDF04F1120003 +:1025400009F071FE401D20F0030511E0011D008817 +:102550000322431821463046FFF789FC00280BD0A0 +:10256000607BABB2684382B26068011D11F05BFB17 +:10257000606841880029E9D170BD70B50E460546F6 +:1025800007F034F8040000D1FFDF012020726672EA +:102590006580207820F00F00C01C20F0F000303063 +:1025A0002070BDE8704007F024B8602801D00720F3 +:1025B00070470878C54900F0010008700020704796 +:1025C0002DE9F0438BB00D461446814606A9FFF76E +:1025D0008AFB002814D14FF6FF7601274FF42058CC +:1025E0008CB103208DF800001020ADF8100007A872 +:1025F000059007AA204604A913F005FA78B1072030 +:102600000BB0BDE8F0830820ADF808508DF80E70CF +:102610008DF80000ADF80A60ADF80C800CE006986B +:10262000A17801742188C1818DF80E70ADF8085031 +:10263000ADF80C80ADF80A606A4602214846069B58 +:10264000FFF77CFBDCE708B501228DF8022042F69B +:102650000202ADF800200A4603236946FFF731FC69 +:1026600008BD08B501228DF8022042F60302ADF83C +:1026700000200A4604236946FFF723FC08BD00B585 +:1026800087B079B102228DF800200A88ADF80820C1 +:102690004988ADF80A1000236A460521FFF74EFB72 +:1026A00007B000BD1020FBE709B1072309E40720AC +:1026B000704770B588B00D461446064606A9FFF768 +:1026C00012FB00280ED17CB10620ADF808508DF821 +:1026D0000000ADF80A40069B6A460821DC813046BE +:1026E000FFF72CFB08B070BD05208DF80000ADF899 +:1026F0000850F0E700B587B059B107238DF80030D6 +:10270000ADF80820039100236A460921FFF716FB64 +:10271000C6E71020C4E770B588B00C460646002511 +:1027200006A9FFF7E0FA0028DCD106980121123053 +:1027300009F0ABFD9CB12178062921D2DFE801F038 +:10274000200505160318801E80B2C01EE28880B2E4 +:102750000AB1A3681BB1824203D90C20C2E7102042 +:10276000C0E7042904D0A08850B901E00620B9E7E9 +:10277000012913D0022905D004291CD005292AD00B +:102780000720AFE709208DF800006088ADF8080049 +:10279000E088ADF80A00A068039023E00A208DF8D5 +:1027A00000006088ADF80800E088ADF80A00A06875 +:1027B0000A25039016E00B208DF800006088ADF824 +:1027C0000800A088ADF80A00E088ADF80C00A06809 +:1027D0000B25049006E00C208DF8000060788DF841 +:1027E00008000C256A4629463046069BFFF7A6FAE4 +:1027F00078E700B587B00D228DF80020ADF80810FD +:1028000000236A461946FFF799FA49E700B587B0F1 +:1028100071B102228DF800200A88ADF8082049889D +:10282000ADF80A1000236A460621FFF787FA37E75A +:10283000102035E770B586B0064601200D46ADF88C +:1028400008108DF80000014600236A463046FFF765 +:1028500075FA040008D12946304605F0B5FC002180 +:10286000304605F0CFFC204606B070BDF8B51C46DA +:1028700015460E46069F11F04AFC2346FF1DBCB2CA +:1028800031462A46009411F036F8F8BD30B41146AE +:10289000DDE902423CB1032903D0002330BC08F03B +:1028A00032BE0123FAE71A8030BC704770B50C467F +:1028B0000546FFF722FB2146284605F094FC2846F2 +:1028C000BDE87040012105F09DBC00001000002013 +:1028D0004FF0E0224FF400400021C2F88001BFF326 +:1028E0004F8FBFF36F8F1748016001601649900248 +:1028F00008607047134900B500220A600A60124B55 +:102900004FF060721A60002808BF00BD0F4A104BDC +:10291000DFF840C001280CD002281CBFFFDF00BD3B +:10292000032008601A604FF4000000BFCCF80000DC +:1029300000BD022008601A604FF04070F6E700B555 +:10294000FFDF00BD00F5004008F50140A4020020B3 +:1029500014F5004004F5014070B50B2000F0BDF9FE +:10296000082000F0BAF900210B2000F0D4F9002172 +:10297000082000F0D0F9F44C01256560A560002026 +:10298000C4F84001C4F84401C4F848010B2000F029 +:10299000B5F9082000F0B2F90B2000F091F925609C +:1029A00070BD10B50B2000F098F9082000F095F9E3 +:1029B000E548012141608160E4490A68002AFCD1B0 +:1029C0000021C0F84011C0F84411C0F848110B2094 +:1029D00000F094F9BDE81040082000F08FB910B560 +:1029E0000B2000F08BF9BDE81040082000F086B9FC +:1029F00000B530B1012806D0022806D0FFDF002044 +:102A000000BDD34800BDD34800BDD248001D00BD65 +:102A100070B5D1494FF000400860D04DC00BC5F8EB +:102A20000803CF4800240460C5F840410820C4359D +:102A300000F053F9C5F83C41CA48047070BD08B5B0 +:102A4000C14A002128B1012811D002281CD0FFDF83 +:102A500008BD4FF48030C2F80803C2F84803BB48F1 +:102A60003C300160C2F84011BDE80840D0E74FF4A7 +:102A70000030C2F80803C2F84803B448403001608F +:102A8000C2F84411B3480CE04FF48020C2F80803A8 +:102A9000C2F84803AD4844300160C2F84811AD485F +:102AA000001D0068009008BD70B516460D4604462E +:102AB000022800D9FFDF0022A348012304F11001FE +:102AC0008B4000EB8401C1F8405526B1C1F840218C +:102AD000C0F8043303E0C0F80833C1F84021C0F85F +:102AE000443370BD2DE9F0411D46144630B1012834 +:102AF00033D0022838D0FFDFBDE8F081891E0022E4 +:102B000021F07F411046FFF7CFFF012D23D0002099 +:102B1000944D924F012668703E61914900203C39E6 +:102B200008600220091D08608D49042030390860C2 +:102B30008B483D34046008206C6000F0DFF83004FE +:102B4000C7F80403082000F0BBF88349F007091F09 +:102B500008602E70D0E70120DAE7012B02D00022B6 +:102B6000012005E00122FBE7012B04D00022022016 +:102B7000BDE8F04198E70122F9E774480068704722 +:102B800070B500F0D8F8704C0546D4F84001002626 +:102B9000012809D1D4F80803C00305D54FF48030CB +:102BA000C4F80803C4F84061D4F8440101280CD1EA +:102BB000D4F80803800308D54FF40030C4F80803A4 +:102BC000C4F84461012013F0EEFED4F84801012856 +:102BD0000CD1D4F80803400308D54FF48020C4F882 +:102BE0000803C4F84861022013F0DDFE5E4805606A +:102BF00070BD70B500F09FF85A4D0446287850B16A +:102C0000FFF706FF687818B10020687013F0CBFE5C +:102C10005548046070BD0320F8E74FF0E0214FF401 +:102C20000010C1F800027047152000F067B84B494A +:102C300001200861082000F061B848494FF47C1079 +:102C4000C1F808030020024601EB8003C3F84025C9 +:102C5000C3F84021401CC0B20628F5D37047410A92 +:102C600043F609525143C0F3080010FB02F000F58F +:102C7000807001EB5020704710B5430B48F2376469 +:102C800063431B0C5C020C60384C03FB0400384BA4 +:102C90004CF2F72443435B0D13FB04F404EB402098 +:102CA00000F580704012107008681844086010BD6C +:102CB0002C484068704729490120C1F8000270473C +:102CC000002809DB00F01F0201219140400980002B +:102CD00000F1E020C0F80011704700280DDB00F083 +:102CE0001F02012191404009800000F1E020C0F85E +:102CF0008011BFF34F8FBFF36F8F7047002809DB40 +:102D000000F01F02012191404009800000F1E02005 +:102D1000C0F8801270474907090E002804DB00F153 +:102D2000E02080F80014704700F00F0000F1E02070 +:102D300080F8141D70470C48001F00680A4A0D49AE +:102D4000121D11607047000000B0004004B5004043 +:102D50004081004044B1004008F50140008000403F +:102D6000408500403C00002014050240F7C2FFFFF0 +:102D70006F0C0100010000010A4810B50468094900 +:102D800009480831086013F0A2FE0648001D0460DF +:102D900010BD0649002008604FF0E0210220C1F874 +:102DA000800270471005024001000001FC1F004036 +:102DB000374901200860704770B50D2000F049F8D0 +:102DC000344C0020C4F800010125C4F804530D2040 +:102DD00000F050F825604FF0E0216014C1F80001C8 +:102DE00070BD10B50D2000F034F82A480121416073 +:102DF0000021C0F80011BDE810400D2000F03AB8E5 +:102E0000254810B504682449244808310860214940 +:102E1000D1F80001012804D0FFDF1F48001D046025 +:102E200010BD1B48001D00680022C0B2C1F800217F +:102E300014F07FFBF1E710B5164800BFD0F8001181 +:102E40000029FBD0FFF7DCFFBDE810400D2000F0AB +:102E500011B800280DDB00F01F020121914040094C +:102E6000800000F1E020C0F88011BFF34F8FBFF366 +:102E70006F8F7047002809DB00F01F02012191408D +:102E80004009800000F1E020C0F880127047000087 +:102E900004D5004000D000401005024001000001B0 +:102EA0004FF0E0214FF00070C1F8800101F5C071D2 +:102EB000BFF34F8FBFF36F8FC1F80001394B8022F2 +:102EC00083F8002441F8800C704700B502460420C6 +:102ED000354903E001EBC0031B792BB1401EC0B2A2 +:102EE000F8D2FFDFFF2000BD41F8302001EBC00128 +:102EF00000224A718A7101220A7100BD2A4A00210A +:102F000002EBC0000171704710B50446042800D3DD +:102F1000FFDF254800EBC4042079012800D0FFDF43 +:102F20006079A179401CC0B2814200D060714FF03D +:102F3000E0214FF00070C1F8000210BD70B504250B +:102F4000194E1A4C16E0217806EBC1000279012ACD +:102F500008D1427983799A4204D04279827156F835 +:102F6000310080472078401CC0B22070042801D373 +:102F7000002020706D1EEDB2E5D270BD0C4810B57A +:102F800004680B490B4808310860064890F80004B3 +:102F90004009042800D0FFDFFFF7D0FF0448001DE0 +:102FA000046010BD19E000E0E0050020580000209A +:102FB00010050240010000010548064A01689142DF +:102FC00001D1002101600449012008607047000020 +:102FD0005C000020BEBAFECA40E5014070B50C4658 +:102FE000054609F02FFC21462846BDE870400AF04E +:102FF00010BD7047704770470021016081807047A5 +:103000002CFFFFFFDBE5B151007002002301FFFF41 +:103010008C00000078DB6A007A2E9AC67DB66CFAC6 +:10302000F35721CCC310D5E51471FB3C30B5FC4DF2 +:103030000446062CA9780ED2DFE804F0030E0E0E2B +:103040000509FFDF08E0022906D0FFDF04E00329BD +:1030500002D0FFDF00E0FFDFAC7030BD30B50446CA +:103060001038EF4D07280CD2DFE800F0040C060CF6 +:103070000C0C0C00FFDF05E0287E112802D0FFDFDA +:1030800000E0FFDF2C7630BD2DE9F04112F026FE86 +:10309000044614F063F8201AC5B2062010F0AEFE04 +:1030A0000446062010F0B2FE211ADD4C207E1228C4 +:1030B00018D000200F18072010F0A0FE06460720A9 +:1030C00010F0A4FE301A3918207E13280CD00020EE +:1030D0000144A078042809D000200844281AC0B26E +:1030E000BDE8F0810120E5E70120F1E70120F4E7E8 +:1030F000CB4810B590F825004108C94800F12600DA +:1031000005D00EF0F5FEBDE8104006F08CB80EF0CC +:10311000D0FEF8E730B50446A1F120000D460A289C +:103120004AD2DFE800F005070C1C2328353A3F445B +:10313000FFDF42E0207820283FD1FFDF3DE0B848A4 +:103140008178052939D0007E122836D020782428AD +:1031500033D0252831D023282FD0FFDF2DE0207851 +:1031600022282AD0232828D8FFDF26E0207822280A +:1031700023D0FFDF21E0207822281ED024281CD075 +:1031800026281AD0272818D0292816D0FFDF14E0C7 +:103190002078252811D0FFDF0FE0207825280CD0DB +:1031A000FFDF0AE02078252807D0FFDF05E0207840 +:1031B000282802D0FFDF00E0FFDF257030BD1FB5FB +:1031C00004466A46002001F0A5FEB4B1BDF8022015 +:1031D0004FF6FF700621824201D1ADF80210BDF812 +:1031E0000420824201D1ADF80410BDF808108142DC +:1031F00003D14FF44860ADF8080068460FF0E2FADA +:1032000006F011F804B010BD70B516460C46054620 +:10321000FEF71FF848B90CB1B44208D90C2070BDB4 +:1032200055F82400FEF715F808B1102070BD2046AF +:10323000641EE4B2F4D270BD2DE9F04105461F468C +:1032400090460E4600240068FEF750F830B9A98871 +:1032500028680844401EFEF749F808B110203FE7EF +:1032600028680028A88802D0B84202D850E0002878 +:10327000F5D0092034E72968085DB8B1671CCA5D3C +:10328000152A2ED03CDC152A3AD2DFE802F039129A +:10329000222228282A2A313139393939393939391C +:1032A00039392200085D30BB641CA4B2A242F9D8AF +:1032B00033E00228DDD1A01C085C88F80000072854 +:1032C00001D2400701D40A200AE7307840F001001B +:1032D00015E0C143C90707E0012807D010E0062028 +:1032E000FEE60107A1F180510029F5D01846F7E666 +:1032F0003078810701D50B20F2E640F002003070F3 +:103300002868005D384484B2A888A04202D2B0E7A1 +:103310004FF4485382B2A242ADD80020E0E610B587 +:10332000027843F2022354080122022C12D003DC5B +:103330003CB1012C16D106E0032C10D07F2C11D10A +:1033400012E0002011E080790324B4EB901F09D132 +:103350000A700BE08079B2EB901F03D1F8E7807917 +:103360008009F5D0184610BDFF200870002010BD60 +:1033700008B500208DF80000294890F82E1051B1B2 +:1033800090F82F0002280FD003280FD0FFDF00BFD6 +:103390009DF8000008BD22486946253001F009FE6D +:1033A0000028F5D0FFDFF3E7032000E001208DF8CF +:1033B0000000EDE738B50C460546694601F0F9FD19 +:1033C00000280DD19DF80010207861F3470020708F +:1033D00055F8010FC4F80100A888A4F805000020E2 +:1033E00038BD38B5137888B102280FD0FF281BD01C +:1033F0000CA46D46246800944C7905EB9414247851 +:1034000064F347031370032805D010E023F0FE0394 +:1034100013700228F7D1D8B240F001000AE0000092 +:10342000F00100200302FF0143F0FE00107010784D +:1034300020F0010010700868C2F801008888A2F826 +:10344000050038BD022110F031BD38B50C460978B1 +:10345000222901D2082038BDADF800008DF80220E5 +:1034600068460EF087FD05F0DEFE050003D1212140 +:103470002046FFF74FFE284638BD1CB500208DF8CA +:103480000000CDF80100ADF80500FB4890F82E00D3 +:10349000022801D0012000E000208DF807006846D6 +:1034A0000EF0F0FD002800D0FFDF1CBD00220A80D6 +:1034B000437892B263F3451222F040020A8000780A +:1034C0000C282BD2DFE800F02A06090E1116191C71 +:1034D0001F220C2742F0110009E042F01D00088075 +:1034E0000020704742F0110012E042F0100040F05E +:1034F0000200F4E742F01000F1E742F00100EEE7CD +:1035000042F0010004E042F00200E8E742F002006D +:1035100040F00400E3E742F00400E0E707207047D2 +:103520002DE9FF478AB00025BDF82C6082461C4675 +:1035300091468DF81C50700703D56068FDF789FE31 +:1035400068B9CD4F4FF0010897F82E0058B197F8A1 +:103550002F00022807D16068FDF7C8FE18B11020BF +:103560000EB0BDE8F087300702D5A08980283DD88D +:10357000700705D4B9F1000F02D097F8240098B372 +:10358000E07DC0F300108DF81B00627D0720032151 +:103590005AB3012A2CD0022AE2D0042AE0D18DF8B5 +:1035A0001710F00627D4A27D072022B3012A22D0CB +:1035B000022A23D0042AD3D18DF819108DF8159042 +:1035C000606810B307A9FFF7AAFE0028C8D19DF8CC +:1035D0001C00FF2816D0606850F8011FCDF80F10AE +:1035E0008088ADF8130014E000E001E00720B7E7A1 +:1035F0008DF81780D5E78DF81980DFE702208DF868 +:103600001900DBE743F20220AAE7CDF80F50ADF82E +:103610001350E07B40B9207C30B9607C20B9A07C9D +:1036200010B9E07CC00601D0062099E78DF800A013 +:10363000BDF82C00ADF80200A0680190A0680290CF +:1036400004F10F0001F0A9FC8DF80C00FFF790FECB +:103650008DF80D009DF81C008DF80E008DF81650A9 +:103660008DF81850E07D08A900F00F008DF81A00C1 +:1036700068460FF0E3F905F0D6FD71E7F0B58FB0BD +:1036800000258DF830508DF814508DF834500646D2 +:103690008DF82850019502950395049519B10FC92D +:1036A00001AC84E80F00744CA078052801D00428F0 +:1036B0000CD101986168884200D120B90398E16873 +:1036C000884203D110B108200FB0F0BD207DC006A4 +:1036D00001D51F2700E0FF273B460DAA05A903A837 +:1036E000FFF7AAFD0028EFD1A08AC10702D0C006CB +:1036F00000D4EE273B460AAA0CA901A8FFF79CFDBF +:103700000028E1D19DF81400C00701D00A20DBE7B2 +:10371000A08A410708D4A17D31B19DF828108907FE +:1037200002D043F20120CFE79DF82810C90709D045 +:10373000400707D4208818B144F25061884201D96B +:103740000720C1E78DF818508DF81960BDF8080002 +:10375000ADF81A000198079006A80FF07BF905F064 +:1037600062FD0028B0D18DF820508DF82160BDF8A1 +:103770001000ADF822000398099008A80FF08CF90A +:1037800005F051FD00289FD101AD241D95E80F00E3 +:1037900084E80F00002097E770B586B00D4604005E +:1037A00005D0FDF7A3FD20B1102006B070BD0820A4 +:1037B000FBE72078C107A98802D0FF2902D303E0E4 +:1037C0001F2901D20920F0E7800763D4FFF75CFCD2 +:1037D00038B12178C1F3C100012804D0032802D0F8 +:1037E00005E01320E1E7244890F82400C8B1C80799 +:1037F0004FF001064FF0000502D08DF80F6001E098 +:103800008DF80F50FFF7B4FD8DF800002078694661 +:10381000C0F3C1008DF8010060788DF80250C20835 +:1038200001D00720C1E730B3C20701D08DF8026094 +:10383000820705D59DF8022042F002028DF8022091 +:10384000400705D59DF8020040F004008DF8020005 +:10385000002022780B18C2F38002DA7001EB4002DC +:103860006388D380401CA388C0B253810228F0D360 +:10387000207A78B905E001E0F00100208DF80260BF +:10388000E6E7607A30B9A07A20B9E07A10B9207BF7 +:10389000C00601D0062088E704F1080001F07DFB96 +:1038A0008DF80E0068460EF0F6FC05F0BCFC002812 +:1038B00089D18DF810608DF81150E088ADF81200B4 +:1038C000ADF8145004A80EF039FD05F0ACFC00284A +:1038D00088D12078C00701D0152000E01320FFF721 +:1038E000BDFB002061E72DE9FF470220FD4E8DF86A +:1038F00004000027708EADF80600B84643F20209B6 +:103900004CE001A810F039FA050006D0708EA8B37B +:10391000A6F83280ADF806803EE0039CA07F010748 +:103920002DD504F124000090A28EBDF80800214698 +:1039300004F1360301F0BCFC050005D04D452AD04A +:10394000112D3CD0FFDF3AE0A07F20F00801E07F9E +:10395000420862F3C711A177810861F30000E077A4 +:1039600094F8210000F01F0084F820002078282817 +:1039700026D129212046FFF7CDFB21E014E04007A6 +:103980000AD5BDF8080004F10E0101F01CFB05008A +:103990000DD04D4510D100257F1CFFB2022010F044 +:1039A0002DFA401CB842ACD8052D11D008E0A07FFC +:1039B00020F00400A07703E0112D00D0FFDF0025E8 +:1039C000BDF806007086052D04D0284604B0C8E571 +:1039D000A6F832800020F9E770B50646FFF732FD01 +:1039E000054605F003FE040000D1FFDF6680207865 +:1039F00020F00F00801C20F0F00020302070032009 +:103A0000207295F83E006072BDE8704005F0F1BD8F +:103A10002DE9F04786B0040000D1FFDF2078B14DDA +:103A200020F00F00801C20F0F000703020706068E3 +:103A30000178491F1B2933D2DFE801F0FE32323210 +:103A400055FD320EFDFD42FC32323278FCFCFBFAB1 +:103A500032FCFCF9F8FCFC00C6883046FFF7F2FCAB +:103A60000546304607F045FCE0B16068007A85F80D +:103A70003E0021212846FFF74DFB3046FEF75AFB5A +:103A8000304603F0D7FE3146012014F017F8A87F26 +:103A900020F01000A877FFF726FF002800D0FFDFF6 +:103AA00006B05EE5207820F0F00020302070032082 +:103AB000207266806068007A607205F09AFDD8E72F +:103AC000C5882846FFF7BEFC00B9FFDF60680079B3 +:103AD000012800D0FFDF6068017A06B02846BDE803 +:103AE000F04707F0EBBDC6883046FFF7ABFC05009A +:103AF00000D1FFDF05F07DFD606831460089288137 +:103B000060684089688160688089A881012013F01D +:103B1000D5FF0020A875A87F00F003000228BFD1C0 +:103B2000FFF7E1FE0028BBD0FFDFB9E7007928B13D +:103B30000228B5D03C28B3D0FFDFB1E705F059FD2E +:103B40006668B6F806A0307A361D012806D0687E71 +:103B5000814605F0D4FA070003D101E0E878F7E7E1 +:103B6000FFDF00220221504610F097F9040000D137 +:103B7000FFDF22212046FFF7CDFA3079012800D05F +:103B80000220A17F804668F30101A177308B20815C +:103B9000708B6081B08BA08184F822908DF80880B2 +:103BA000B8680090F86801906A460321504610F00A +:103BB00074F900B9FFDFB888ADF81000B8788DF857 +:103BC000120004AA0521504610F067F900B9FFDF82 +:103BD000B888ADF80C00F8788DF80E0003AA04211F +:103BE000504610F05AF900B9FFDF062106F1120025 +:103BF0000DF00EF940B37079800700D5FFDF7179C1 +:103C0000E07D61F34700E075D6F80600A061708999 +:103C1000A083062106F10C000DF0FAF8F0B195F83A +:103C200025004108607861F3470006E041E039E093 +:103C300071E059E04EE02FE043E06070D5F82600D7 +:103C4000C4F80200688D12E0E07D20F0FE00801CC8 +:103C5000E075D6F81200A061F08AD9E7607820F00C +:103C6000FE00801C6070F068C4F80200308AE080BA +:103C7000B8F1010F04D0B8F1020F05D0FFDF0FE754 +:103C80000320FFF7D3F90BE7287E122800D0FFDFCF +:103C90001120FFF7E3F903E706B02046BDE8F0473F +:103CA00001F092BD05F0A5FC15F8300F40F00200C0 +:103CB00005E005F09EFC15F8300F40F00400287078 +:103CC000EEE6287E13280AD01528D8D15FF016001A +:103CD000FFF7C4F906B0BDE8F04705F08ABC142030 +:103CE000F6E70000F0010020A978052909D0042991 +:103CF000C5D105F07EFC022006B0BDE8F047FFF715 +:103D000095B900790028BAD0E87801F02DF905F0CE +:103D100070FC0320F0E7287E122802D1687E01F0B3 +:103D200023F91120D4E72DE9F05F054600784FF024 +:103D300000080009DFF8B8A891460C46464601285D +:103D40006ED002286DD007280BD00A286AD0FFDF7A +:103D5000A9F8006014B1A4F8008066800020BDE8D6 +:103D6000F09F6968012704F108000B784FF0020BFF +:103D70005B1F4FF6FF721B2B7ED2DFE803F0647DE2 +:103D80007D7D0E7D7D7D7D7D7D217D7D7D2BFDFC81 +:103D9000FBFA7D14D2F9E7F8F700C8884FF0120853 +:103DA000102621469AE14FF01C080A26BCB38888E9 +:103DB000A0806868807920726868C0796072C7E7FF +:103DC0004FF01B08142654B30320207268688088C3 +:103DD000A080BDE70A793C2ABAD00D1D4FF010082B +:103DE0002C26E4B16988A180298B6182298B2182EC +:103DF000698BA182A98BE1826B790246A91D1846C5 +:103E0000FFF7EFFA2979002001290CD084F80FB0D0 +:103E1000FF212176E06120626062A06298E70FE0F6 +:103E20003BE15EE199E1E77320760AF1040090E856 +:103E30000E00DAF81000C4E90930C4E9071287E778 +:103E4000A9F800608AE72C264FF01D08002CF7D057 +:103E5000A28005460F1D897B008861F30000288041 +:103E6000B97A490861F341002880B97A890861F379 +:103E700082002880B97A00E00CE1C90861F3C30030 +:103E80002880B97AAA1C0911491C61F3041000F0BA +:103E90007F0028807878B91CFFF7A3FA387D05F1F8 +:103EA000090207F11501FFF79CFA387B01F0A9F828 +:103EB0002874787B01F0A5F86874F87EA874787A85 +:103EC000E874387F2875B87B6875388AE882DAF834 +:103ED0001C10A961B97A504697F808A0C1F34111A6 +:103EE000012904D0008C504503D2824609E0FFDF4F +:103EF00010E0022903D0288820F0600009E0504536 +:103F000004D1288820F06000403002E0288840F08A +:103F100060002880A4F824A0524607F11D01A8697A +:103F20009BE011264FF02008002C89D0A280686801 +:103F300004F10A02007920726868007B6072696887 +:103F40008B1D48791946FFF74CFA01E70A264FF016 +:103F50002108002CE9D08888A080686880792072C8 +:103F60006868C07960729AF8301006E078E06BE01B +:103F700052E07FE019E003E03AE021F00401A6E01E +:103F80000B264FF02208002CCFD0C888A08068688C +:103F9000007920726868007A01F033F8607268680E +:103FA000407A01F02EF8A072D2E61C264FF02608C7 +:103FB000002CBAD0A2806868407960726868007A84 +:103FC000A0720AF1040090E80E00DAF81000C4E9CB +:103FD0000530C4E90312686800793C2803D04328FF +:103FE00003D0FFDFB4E62772B2E684F808B0AFE68C +:103FF00010264FF02408002C97D08888A08068688D +:10400000807920816868807A608168680089A081F1 +:1040100068688089E0819BE610264FF02308002C19 +:1040200098D08888A0806868C088208168680089E6 +:10403000608168684089A08168688089E0819AF819 +:10404000301021F0020142E030264FF02508002C0C +:104050009AD0A2806968282249680AF0EEF977E6CA +:104060002A264FF02F08002C8ED0A28069682222C9 +:10407000091DF2E714264FF01B08002C84D0A28003 +:10408000686800790128B0D02772DAE90710C4E91E +:1040900003105DE64A46214660E0287A012803D0F5 +:1040A000022817D0FFDF53E610264FF01F08002C20 +:1040B000A2D06888A080A8892081E8896081288AA8 +:1040C000A081688AE0819AF8301021F001018AF815 +:1040D00030103DE64FF012081026688800F07EFF91 +:1040E00036E6287AC8B3012838D0022836D003280B +:1040F00001D0FFDF2CE609264FF01108002C8FD0ED +:104100006F883846FFF79EF990F822A0A780687A5A +:104110002072042138460FF0DBFE052138460FF0EF +:10412000D7FE002138460FF0D3FE012138460FF0AC +:10413000CFFE032138460FF0CBFE022138460FF0A8 +:10414000C7FE062138460FF0C3FE072138460FF0A0 +:10415000BFFE504600F008FFFAE5FFE72846BDE83D +:10416000F05F01F0BBBC70B5012803D0052800D07A +:10417000FFDF70BD8DB22846FFF764F9040000D15F +:10418000FFDF20782128F4D005F030FA80B10178E3 +:1041900021F00F01891C21F0F00110310170022182 +:1041A000017245800020A075BDE8704005F021BA7D +:1041B00021462846BDE870401322FFF746B92DE995 +:1041C000F04116460C00804600D1FFDF307820F029 +:1041D0000F00801C20F0F000103030702078012893 +:1041E00004D0022818D0FFDFBDE8F0814046FFF779 +:1041F00029F9050000D1FFDF0320A87505F0F9F9C2 +:1042000094E80F00083686E80F00F94810F8301FD0 +:1042100041F001010170E7E74046FFF713F905009F +:1042200000D1FFDFA1884FF6FF700027814202D145 +:10423000E288824203D0814201D1E08840B105F09A +:10424000D8F994E80F00083686E80F00AF75CBE781 +:10425000A87D0128C8D178230022414613F084FBB1 +:104260000220A875C0E738B50C4624285CD008DCCD +:1042700020280FD0212825D022284BD0232806D152 +:104280004CE0252841D0262832D03F2851D00725A0 +:10429000284638BD0021052013F0E6FB08B11120A7 +:1042A00038BDA01C0EF0E1FA04F0BDFF0500EFD10F +:1042B000002208231146052013F056FB0528E7D0FD +:1042C000FFDFE5E76068FDF708F808B1102038BDAA +:1042D000618820886A460EF071FD04F0A4FF050095 +:1042E000D6D160680028D3D0BDF800100180CFE798 +:1042F000206820B1FCF7FAFF08B11025C8E7204676 +:104300000EF03BFE1DE00546C2E7A17820880EF0C6 +:1043100086FD16E0086801F08DFEF4E7087800F0ED +:1043200001000DF0B9FD0CE0618820880EF0C1FCA1 +:1043300007E0087800F001008DF8000068460EF0F4 +:10434000DFF804F070FFDEE770B505460C4608465E +:10435000FCF7A5FF08B1102070BD202D07D0212D3E +:104360000DD0222D0BD0252D09D0072070BD20881F +:10437000A11C0DF065FEBDE8704004F054BF06209E +:1043800070BD9B482530704708B5342200219848FD +:104390000AF07DF80120FEF749FE1120FEF75EFECF +:1043A00093496846263105F0B7F891489DF80020FA +:1043B00010F8251F62F3470121F00101017000216F +:1043C00041724FF46171A0F8071002218172FEF76B +:1043D0008FFE00B1FFDFFDF705F801F0BEF908BD63 +:1043E00010B50C464022002120460AF050F8A07F6C +:1043F00020F00300A077202020700020A07584F812 +:10440000230010BD70472DE9FC410746FCF721FF52 +:1044100010B11020BDE8FC81754E06F12501D6F8DB +:1044200025000090B6F82950ADF8045096F82B40BE +:104430008DF806403846FEF7BDFF0028EAD1FEF7AA +:1044400057FE0028E6D0009946F8251FB580B471C4 +:10445000E0E710B50446FCF722FF08B1102010BDBC +:1044600063486349224690F8250026314008FEF74C +:10447000B8FF002010BD3EB504460D460846FCF7C7 +:104480000EFF08B110203EBD14B143F204003EBD42 +:1044900057488078052803D0042801D008203EBD65 +:1044A000694602A80AF012FC2A4669469DF80800EF +:1044B000FEF797FF00203EBDFEB50D4604004FF00D +:1044C000000712D00822FEF79FFE002812D1002616 +:1044D00009E000BF54F826006946FEF720FF0028D7 +:1044E00008D1761CF6B2AE42F4D30DF01CFC10B12C +:1044F00043F20320FEBD3E4E86F824700CB3002725 +:104500001BE000BF54F8270002A9FEF708FF00B126 +:10451000FFDF9DF808008DF8000054F8270050F8E0 +:10452000011FCDF801108088ADF8050068460DF038 +:104530001FFC00B1FFDF7F1CFFB2AF42E2D386F861 +:1045400024500020FEBD2DE9F0418AB01546884672 +:1045500004001ED00F4608222946FEF755FE00280B +:1045600011D1002613E000BF54F826006946103030 +:1045700000F01FFD002806D13FB157F82600FCF7D8 +:1045800068FE10B110200AB02EE6761CF6B2AE42DC +:10459000EAD3681EC6B217E0701CC7B212E000BFB3 +:1045A00054F82600017C4A0854F827100B7CB2EB23 +:1045B000530F05D106221130113109F011FF50B10E +:1045C0007F1CFFB2AF42EBD3761EF6B2E4D2464672 +:1045D00024B1012003E043F20520D4E700200DF0D0 +:1045E000ECFB10B90DF0F5FB20B143F20420CAE753 +:1045F000F001002064B300270DF1170826E000BF8A +:1046000054F827006946103000F0D3FC00B1FFDFFA +:1046100054F82700102250F8111FCDF8011080889F +:10462000ADF8050054F827100DF1070009F005FF5B +:1046300096B156F827101022404609F0FEFE684653 +:104640000DF07BFB00B1FFDF7F1CFFB2AF42D7D381 +:10465000FEF713FF002096E7404601F0DFFCEEE78F +:1046600030B585B00446FDF7BDF830B906200FF02F +:10467000C5FB10B1062005B030BD2046FCF7E9FDB2 +:1046800018B96068FCF732FE08B11020F3E76088C3 +:104690004AF2B811884206D82078F94D28B101288D +:1046A00006D0022804D00720E5E7FEF721FD18E038 +:1046B0006078022804D0032802D043F20220DAE70F +:1046C00085F82F00C1B200200090ADF80400022947 +:1046D0002CD0032927D0FFDF68460DF009FC04F039 +:1046E000A2FD0028C7D1606801F08BFC207858B18A +:1046F00001208DF800000DF1010001F08FFC6846EB +:104700000EF0FDFB00B1FFDF207885F82E00FEF7EC +:10471000B4FE608860B1A88580B20DF046FB00B1A0 +:10472000FFDF0020A7E78DF80500D5E74020FAE776 +:104730004FF46170EFE710B50446FCF7B0FD20B907 +:10474000606838B1FCF7C9FD08B1102010BD606881 +:1047500001F064FCCA4830F82C1F6180C178617098 +:1047600080782070002010BD2DE9F843144689465A +:104770000646FCF794FDA0B94846FCF7B7FD80B9A2 +:104780002046FCF7B3FD60B9BD4DA878012800D1E3 +:104790003CB13178FF2906D049B143F20400BDE8AD +:1047A000F8831020FBE7012801D00420F7E7CCB301 +:1047B000052811D004280FD069462046FEF776FE62 +:1047C0000028ECD1217D49B1012909D0022909D065 +:1047D000032909D00720E2E70820E0E7024604E0C9 +:1047E000012202E0022200E003228046234617460F +:1047F00000200099FEF794FE0028D0D1A0892880DF +:10480000A07BE875BDF80000A882AF75BDF8001068 +:10481000090701D5A18931B1A1892980C00704D038 +:10482000032003E006E08021F7E70220FEF7FEFB0D +:1048300086F800804946BDE8F8430020FEF71EBF19 +:104840007CB58F4C05460E46A078022803D003287D +:1048500001D008207CBD15B143F204007CBD0720C7 +:104860000FF0D4FA10B9A078032806D0FEF70CFC9C +:1048700028B1A078032804D009E012207CBD1320C1 +:104880007CBD304600F053FB0028F9D1E670FEF7FE +:104890006FFD0AF058F901208DF800008DF8010035 +:1048A0008DF802502088ADF80400E07D8DF80600F8 +:1048B00068460EF0C1F904F0B6FC0028E0D1A078FB +:1048C000032805D05FF00400FEF7B0FB00207CBD9C +:1048D000E07800F03CFB0520F6E71CB510B143F290 +:1048E00004001CBD664CA078042803D0052801D024 +:1048F00008201CBD00208DF8000001218DF801105A +:104900008DF8020068460EF097F904F08CFC002840 +:10491000EFD1A078052805D05FF00200FEF786FBF6 +:1049200000201CBDE07800F01FFB0320F6E72DE916 +:10493000FC4180460E4603250846FCF7D7FC0028BC +:1049400066D14046FEF77EFD040004D02078222880 +:1049500004D208205EE543F202005BE5A07F00F090 +:1049600003073EB1012F0CD000203146FEF727FC93 +:104970000500EFD1012F06D0022F1AD0FFDF284605 +:1049800048E50120F1E7A07D3146022801D011B1B0 +:1049900007E011203EE56846FCF758FE0028D9D113 +:1049A0006946404606F04DFE0500E8D10120A0759D +:1049B000E5E7A07D032804D1314890F83000C00716 +:1049C00001D02EB30EE026B1A07F40071ED40021F7 +:1049D00000E00121404606F054FE0500CFD1A0754D +:1049E000002ECCD03146404600F0EDFA05461128A5 +:1049F000C5D1A07F4107C2D4316844F80E1F716849 +:104A0000616040F0040020740025B8E71125B6E786 +:104A10001020FFE470B50C460546FEF713FD0100BB +:104A200005D022462846BDE87040FEF70EBD43F291 +:104A3000020070BD10B5012807D1114B9B78012BE6 +:104A400000D011B143F2040010BD0DF0E0F9BDE853 +:104A5000104004F0E8BB012300F090BA00231A468E +:104A6000194600F08BBA70B506460C460846FCF7AE +:104A7000F0FB18B92068FCF712FC18B1102070BDCB +:104A8000F0010020F84D2A7E112A04D0132A00D309 +:104A90003EB10820F3E721463046FEF77DFE60B1C7 +:104AA000EDE70920132A0DD0142A0BD0A188FF2985 +:104AB000E5D31520FEF7D2FA0020D4E90012C5E9AB +:104AC0000712DCE7A1881F29D9D31320F2E71CB510 +:104AD000E548007E132801D208201CBD00208DF877 +:104AE000000068460DF02AFC04F09DFB0028F4D17C +:104AF0001120FEF7B3FA00201CBD2DE9F04FDFF8BE +:104B000068A3814691B09AF818009B4615460C465A +:104B1000132803D3FFF7DBFF00281FD12046FCF743 +:104B200098FBE8BB2846FCF794FBC8BB20784FF005 +:104B30000107C0074FF0000102D08DF83A7001E084 +:104B40008DF83A1020788846C0F3C1008DF8000037 +:104B500060788DF80910C10803D0072011B0BDE8B6 +:104B6000F08FB0B3C10701D08DF80970810705D56A +:104B70009DF8091041F002018DF80910400705D594 +:104B80009DF8090040F004008DF809009DF8090027 +:104B9000810703D540F001008DF80900002000E0F6 +:104BA00015E06E4606EB400162884A81401CA288EF +:104BB000C0B20A820328F5D32078C0F3C1000128CF +:104BC00025D0032823D04846FCF743FB28B110200A +:104BD000C4E7FFE78DF80970D8E799F800004008AE +:104BE00008D0012809D0022807D0032805D043F2B5 +:104BF0000220B3E78DF8028001E08DF8027048468C +:104C000050F8011FCDF803108088ADF80700FEF7BB +:104C1000AFFB8DF801000021424606EB41002B88D6 +:104C2000C3826B888383AB884384EB880385491CEC +:104C3000C285C9B282860329EFD3E088ADF83C0073 +:104C400068460DF053FC002887D19AF818005546A5 +:104C5000112801D0082081E706200FF0D7F838B1DD +:104C60002078C0F3C100012804D0032802D006E058 +:104C7000122073E795F8240000283FF46EAFFEF78A +:104C800003FA022801D2132068E7584600F04FF9D2 +:104C900000289DD185F819B068460DF06DFD04F02F +:104CA000C2FA040094D1687E00F051F91220FEF798 +:104CB000D5F9204652E770B56B4D287E122801D0F9 +:104CC0000820DCE60DF05BFD04F0ADFA040005D130 +:104CD000687E00F049F91120FEF7C0F92046CEE6C3 +:104CE00070B5064615460C460846FCF7D8FA18B9C2 +:104CF0002846FCF7D4FA08B11020C0E62A4621461F +:104D000030460EF03BF804F08EFA0028F5D12178F9 +:104D10007F29F2D10520B2E67CB505460C4608464F +:104D2000FCF797FA08B110207CBD2846FEF78AFBF5 +:104D300020B10078222804D208207CBD43F2020072 +:104D40007CBD494890F83000400701D511207CBD5A +:104D50002078C00802D16078C00801D007207CBD4F +:104D6000ADF8005020788DF8020060788DF80300CF +:104D70000220ADF8040068460CF03BFE04F053FA44 +:104D80007CBD70B586B014460D460646FEF75AFB4C +:104D900028B10078222805D2082006B06FE643F239 +:104DA0000200FAE72846FCF7A1FA20B944B12046F0 +:104DB000FCF793FA08B11020EFE700202060A080F4 +:104DC000294890F83000800701D51120E5E703A9B4 +:104DD00030460CF05EFE10B104F025FADDE7ADF8C8 +:104DE0000060BDF81400ADF80200BDF81600ADF883 +:104DF0000400BDF81000BDF81210ADF80600ADF8C3 +:104E000008107DB1298809B1ADF80610698809B18B +:104E1000ADF80210A98809B1ADF80810E98809B108 +:104E2000ADF80410DCB1BDF80610814201D9081AB2 +:104E30002080BDF80210BDF81400814201D9081A83 +:104E40006080BDF80800BDF80410BDF816200144CC +:104E5000BDF812001044814201D9081AA0806846AA +:104E60000CF0D5FEB8E70000F00100201CB56C493D +:104E70000968CDE9001068460DF03AFB04F0D3F95B +:104E80001CBD1CB500200090019068460DF030FB61 +:104E900004F0C9F91CBD70B505460C460846FCF780 +:104EA000FEF908B11020EAE5214628460DF012F976 +:104EB000BDE8704004F0B7B93EB505460C4608465B +:104EC000FCF7EDF908B110203EBD002000900190E4 +:104ED0000290ADF800502089ADF8080020788DF8D8 +:104EE0000200606801902089ADF808006089ADF883 +:104EF0000A0068460DF000F904F095F93EBD0EB5C4 +:104F0000ADF800000020019068460DF0F5F804F0BF +:104F10008AF90EBD10800888508048889080C88823 +:104F200010818888D080002050819081704710B512 +:104F3000044604F0E4F830B1407830B1204604F083 +:104F4000FCFB002010BD052010BD122010BD10B5C7 +:104F500004F0D5F8040000D1FFDF607800B9FFDF6E +:104F60006078401E607010BD10B504F0C8F80400F1 +:104F700000D1FFDF6078401C607010BD1CB5ADF83B +:104F800000008DF802308DF803108DF8042068467B +:104F90000DF0B7FE04F047F91CBD0CB521A2D2E913 +:104FA0000012CDE900120079694601EB501000783B +:104FB0000CBD0278520804D0012A02D043F202202C +:104FC0007047FEF7ACB91FB56A46FFF7A3FF684606 +:104FD0000DF008FC04F027F904B010BD70B50C000A +:104FE00006460DD0FEF72EFA050000D1FFDFA680A1 +:104FF00028892081288960816889A081A889E08129 +:105000003DE500B540B1012805D0022803D00328B2 +:1050100004D0FFDF002000BDFF2000BD042000BD44 +:1050200014610200070605040302010010B50446DE +:10503000FCF70FF908B1102010BD2078C0F3021062 +:10504000042807D86078072804D3A178102901D84C +:10505000814201D2072010BDE078410706D42179B2 +:105060004A0703D4000701D4080701D5062010BD64 +:10507000002010BD10B513785C08837F64F3C7135C +:10508000837713789C08C37F64F30003C377107899 +:10509000C309487863F34100487013781C090B7802 +:1050A00064F347130B701378DB0863F30000487058 +:1050B0005078487110BD10B5C4780B7864F30003C4 +:1050C0000B70C478640864F341030B70C478A408BF +:1050D00064F382030B70C478E40864F3C3030B70B9 +:1050E0000379117863F30001117003795B0863F3AE +:1050F0004101117003799B0863F3820111700079FB +:10510000C00860F3C301117010BD70B514460D46A0 +:10511000064604F06BFA80B10178182221F00F01E5 +:10512000891C21F0F001A03100F8081B214609F08C +:1051300084F9BDE8704004F05CBA29463046BDE809 +:1051400070401322FEF781B92DE9F047064608A802 +:10515000904690E8300489461F46142200212846D4 +:1051600009F095F90021CAF80010B8F1000F03D03A +:10517000B9F1000F03D114E03878C00711D02068CE +:10518000FCF78DF8C0BBB8F1000F07D120681230D2 +:1051900028602068143068602068A8602168CAF818 +:1051A00000103878800724D56068FCF796F818BBA3 +:1051B000B9F1000F21D0FFF7E4F80168C6F86811D3 +:1051C0008188A6F86C11807986F86E0101F013FDD4 +:1051D000F94FEF60626862B196F8680106F26911F2 +:1051E00040081032FEF7FDF810223946606809F0D9 +:1051F00024F90020BDE8F08706E0606820B1E8608F +:105200006068C6F86401F4E71020F3E730B505469E +:1052100008780C4620F00F00401C20F0F0011031FF +:1052200021700020607095F8230030B104280FD061 +:10523000052811D0062814D0FFDF20780121B1EB1A +:10524000101F04D295F8200000F01F00607030BDE0 +:1052500021F0F000203002E021F0F000303020702A +:10526000EBE721F0F0004030F9E7F0B591B002270C +:1052700015460C4606463A46ADF80870092103ABC0 +:1052800005F063F80490002810D004208DF8040085 +:105290008DF80170E034099605948DF818500AA92C +:1052A000684610F0FCFA00B1FFDF012011B0F0BD3C +:1052B00010B588B00C460A99ADF80000CBB118685B +:1052C000CDF80200D3F80400CDF80600ADF80A20AE +:1052D000102203A809F0B1F868460DF0F3FA03F0C4 +:1052E000A2FF002803D1A17F41F01001A17708B0EF +:1052F00010BD0020CDF80200E6E72DE9F84F064684 +:10530000808A0D4680B28246FEF79CF804463078CB +:10531000DFF8A48200274FF00209A8F120080F2827 +:1053200070D2DFE800F06FF23708387D8CC8F1F0FA +:10533000EFF35FF3F300A07F00F00300022809D031 +:105340005FF0000080F0010150460EF0AFFD050057 +:1053500003D101E00120F5E7FFDF98F85C10C907F1 +:1053600002D0D8F860000BE0032105F11D0012F017 +:10537000BEF8D5F81D009149B0FBF1F201FB120017 +:10538000C5F81D0070686867B068A8672078252890 +:1053900000D0FFDFCAE0A07F00F00300022809D0A0 +:1053A0005FF0000080F0010150460EF07FFD060026 +:1053B00003D101E00120F5E7FFDF3078810702D556 +:1053C0002178252904D040F001003070BDE8F88F25 +:1053D00085F80090307F287106F11D002D36C5E953 +:1053E0000206F3E7A07F00F00300022808D00020A7 +:1053F00080F0010150460EF059FD040004D102E096 +:105400000120F5E7A7E1FFDF2078C10604D50720DA +:1054100028703D346C60D9E740F008002070D5E773 +:10542000E07F000700D5FFDF307CB28800F0010389 +:1054300001B05046BDE8F04F092106F064B804B948 +:10544000FFDF716821B1102204F1240008F0F5FF9C +:1054500028212046FDF75EFEA07F00F00300022811 +:105460000ED104F12400002300901A462146504634 +:10547000FFF71EFF112807D029212046FDF74AFE1D +:10548000307A84F82000A1E7A07F000700D5FFDF75 +:1054900014F81E0F40F008002070E782A761E76152 +:1054A000C109607861F34100014660F382016170D7 +:1054B000307AE0708AE7A07F00F00300022809D06C +:1054C0005FF0000080F0010150460EF0EFFC040098 +:1054D00003D101E00120F5E7FFDF022104F185009F +:1054E00012F005F80420287004F5B4706860B4F870 +:1054F00085002882304810387C346C61C5E9028010 +:1055000064E703E024E15BE02DE015E0A07F00F01C +:105510000300022807D0002080F0010150460EF061 +:10552000C5FC18B901E00120F6E7FFDF324621464D +:105530005046BDE8F84FE8E504B9FFDF20782128A0 +:10554000A1D93079012803D1E07F40F00800E0774D +:10555000324621465046FFF7D8FD2046BDE8F84FB9 +:105560002321FDF7D7BD3279AA8005F1080309216F +:10557000504604F0EAFEE86010B10520287025E7E7 +:10558000A07F00F00300022808D0002080F0010175 +:1055900050460EF08BFC040003D101E00120F5E73A +:1055A000FFDF04F1620102231022081F0EF005FB49 +:1055B00007703179417009E75002002040420F0026 +:1055C000A07F00F00300022808D0002080F0010135 +:1055D00050460EF06BFC050003D101E00120F5E719 +:1055E000FFDF95F8840000F0030001287AD1A07F46 +:1055F00000F00307E07F10F0010602D0022F04D173 +:1056000033E095F8A000C0072BD0D5F8601121B386 +:1056100095F88320087C62F387000874A17FCA098B +:10562000D5F8601162F341000874D5F8601166F393 +:1056300000000874AEB1D5F86001102204F1240115 +:10564000883508F0FAFE287E40F001002876287898 +:1056500020F0010005F8880900E016B1022F04D0FF +:105660002DE095F88800C00727D0D5F85C1121B34C +:1056700095F88320087C62F387000874A17FCA092B +:10568000D5F85C1162F341000874D5F85C1166F33B +:10569000000008748EB1D5F85C01102204F12401D9 +:1056A000883508F0CAFE287840F0010005F8180B8C +:1056B000287820F0010005F8A009022F44D000202E +:1056C00000EB400005EBC00090F88800800709D58A +:1056D00095F87C00D5F86421400805F17D01103271 +:1056E000FDF77FFE8DF8009095F884006A4600F083 +:1056F00003008DF8010095F888108DF8021095F8D8 +:10570000A0008DF803002146504601F05DFA207894 +:10571000252805D0212807D0FFDF2078222803D9AB +:1057200022212046FDF7F6FCA07F00F003000228AE +:105730000CD0002080F0010150460EF0C9FB00287B +:105740003FF44FAEFFDF41E60120B9E70120F1E76A +:10575000706847703AE6FFDF38E670B5FE4C00250A +:1057600084F85C50256610F066F804F110012046BC +:1057700003F0F8FE84F8305070BD70B50D46FDF7AB +:1057800061FE040000D1FFDF4FF4B872002128460B +:1057900008F07DFE04F124002861A07F00F00300E2 +:1057A000022809D05FF0010105F1E00010F044F893 +:1057B000002800D0FFDF70BD0221F5E70A46014650 +:1057C00002F1E00010F059B870B50546406886B0A7 +:1057D00001780A2906D00D2933D00E292FD0FFDFFA +:1057E00006B070BD86883046FDF72CFE040000D15F +:1057F000FFDF20782128F3D028281BD168680221F8 +:105800000E3001F0D6F9A8B168680821801D01F0BA +:10581000D0F978B104F1240130460DF00FFA03F00D +:1058200002FD00B1FFDF06B02046BDE8704029212F +:10583000FDF770BC06B0BDE8704003F0DABE012190 +:1058400001726868C6883046FDF7FCFD040000D18F +:10585000FFDFA07F00F00301022902D120F0100039 +:10586000A077207821280AD06868017A09B10079E8 +:1058700080B1A07F00F00300022862D0FFDFA07F8C +:1058800000F003000228ABD1FEF72DF80028A7D0C6 +:10589000FFDFA5E703F0ADFEA17F08062BD5E07F73 +:1058A000C00705D094F8200000F01F00102820D079 +:1058B0005FF0050084F82300207829281DD02428D3 +:1058C000DDD13146042012F0F9F822212046FDF7FF +:1058D00021FCA07F00F00300022830D05FF0000020 +:1058E00080F0010130460EF0F3FA0028C7D0FFDF48 +:1058F000C5E70620DEE70420DCE701F0030002280C +:1059000008D0002080F0010130460EF0CFFA0500EB +:1059100003D101E00120F5E7FFDF25212046FDF757 +:10592000F9FB03208DF80000694605F1E0000FF057 +:105930009BFF0228A3D00028A1D0FFDF9FE7012012 +:10594000CEE703F056FE9AE72DE9F04387B099467B +:10595000164688460746FDF775FD04004BD02078B3 +:10596000222848D3232846D0E07F000743D4A07FD5 +:1059700000F00300022809D05FF0000080F0010170 +:1059800038460EF093FA050002D00CE00120F5E74E +:10599000A07F00F00300022805D001210022384634 +:1059A0000EF07BFA05466946284601F034F9009866 +:1059B00000B9FFDF45B10098E03505612078222865 +:1059C00006D0242804D007E000990020086103E0F5 +:1059D00025212046FDF79EFB00980121417047627A +:1059E000868001A9C0E902890FF059FF022802D080 +:1059F000002800D0FFDF07B0BDE8F08370B586B0A7 +:105A00000546FDF71FFD017822291ED9807F00F091 +:105A10000300022808D0002080F0010128460EF083 +:105A200045FA04002FD101E00120F5E7FFDF2AE06D +:105A3000B4F85E0004F1620630440178427829B17E +:105A400021462846FFF711FCB0B9C9E6ADF804209D +:105A50000921284602AB04F078FC03900028F4D01A +:105A600005208DF80000694604F1E0000FF0FCFE0F +:105A7000022801D000B1FFDF02231022314604F1D9 +:105A80005E000EF0D0F8B4F860000028D0D1A7E690 +:105A900010B586B00446FDF7D5FC017822291BD944 +:105AA000807F00F00300022808D0002080F0010170 +:105AB00020460EF0FBF9040003D101E00120F5E7D8 +:105AC000FFDF06208DF80000694604F1E0000FF0CA +:105AD000CBFE002800D0FFDF06B010BD2DE9F05F3F +:105AE00005460C4600270078904601093E4604F121 +:105AF000080BBA4602297DD0072902D00A2909D10C +:105B000046E0686801780A2905D00D2930D00E29B1 +:105B10002ED0FFDFBBE114271C26002C6BD0808821 +:105B2000A080FDF78FFC5FEA000900D1FFDF99F844 +:105B300017005A46400809F11801FDF752FC686841 +:105B4000C0892082696851F8060FC4F812004868BD +:105B5000C4F81600A07E01E03002002020F006000C +:105B600040F00100A07699F81E0040F020014DE0C1 +:105B70001A270A26002CD1D0C088A080FDF762FC2D +:105B8000050000D1FFDF59462846FFF73FFB7EE1C5 +:105B90000CB1A88BA080287A0B287DD006DC0128C8 +:105BA0007BD0022808D0032804D135E00D2875D019 +:105BB0000E2874D0FFDF6AE11E270926002CADD025 +:105BC000A088FDF73FFC5FEA000900D1FFDF287BDA +:105BD00000F003000128207A1BD020F00100207281 +:105BE000297B890861F341002072297BC90861F390 +:105BF000820001E041E1F2E02072297B090961F3B2 +:105C0000C300207299F81E0040F0400189F81E1070 +:105C10003DE140F00100E2E713270D26002CAAD059 +:105C2000A088FDF70FFC8146807F00F0030002286A +:105C300008D0002080F00101A0880EF037F905009F +:105C400003D101E00120F5E7FFDF99F81E0000F025 +:105C50000302022A50D0686F817801F00301012904 +:105C6000217A4BD021F00101217283789B0863F3E4 +:105C7000410121728378DB0863F38201217283780A +:105C80001B0963F3C3012172037863F306112172C8 +:105C9000437863F3C71103E061E0A9E090E0A1E07D +:105CA000217284F809A0C178A172022A29D0027950 +:105CB000E17A62F30001E1720279520862F3410174 +:105CC000E1720279920862F38201E1720279D208EC +:105CD00062F3C301E1724279217B62F30001217317 +:105CE0004279520862F3410121734279920862F3CA +:105CF00082012173407928E0A86FADE741F00101EE +:105D0000B2E74279E17A62F30001E1724279520826 +:105D100062F34101E1724279920862F38201E17219 +:105D20004279D20862F3C301E1720279217B62F306 +:105D3000000121730279520862F341012173027953 +:105D4000920862F3820121730079C00860F3C301F5 +:105D5000217399F80000232831D9262140E0182723 +:105D60001026E4B3A088FDF76DFB8346807F00F02A +:105D70000300022809D0002080F00101A0880EF065 +:105D800095F85FEA000903D101E00120F4E7FFDFA5 +:105D9000E868A06099F8000040F0040189F800105C +:105DA00099F80100800708D5012020739BF80000B6 +:105DB00023286CD92721584651E084F80CA066E0CE +:105DC00015270F265CB1A088FDF73CFB8146062213 +:105DD0005946E86808F0C7FB0120A073A0E041E045 +:105DE00048463CE016270926E4B3287B20724EE0A3 +:105DF000287B19270E26ACB3C4F808A0A4F80CA081 +:105E0000012807D0022805D0032805D0042803D094 +:105E1000FFDF0DE0207207E0697B042801F00F012D +:105E200041F0800121721ED0607A20F00300607280 +:105E3000A088FDF707FB05460078212827D02328F6 +:105E400000D0FFDFA87F00F00300022813D000205D +:105E500080F00101A0880EF03BF822212846FDF7D2 +:105E600059F914E004E0607A20F00300401CDEE7FA +:105E7000A8F8006010E00120EAE70CB16888A08073 +:105E8000287A68B301280AD002284FD0FFDFA8F88B +:105E900000600CB1278066800020BDE8F09F1527C8 +:105EA0000F26002CE4D0A088FDF7CCFA807F00F00C +:105EB0000300022808D0002080F00101A0880DF026 +:105EC000F5FF050003D101E00120F5E7FFDFD5F87C +:105ED0001D000622594608F046FB84F80EA0D6E7BE +:105EE00017270926002CC3D0A088FDF7ABFA8146FE +:105EF000807F00F00300022808D0002080F001011C +:105F0000A0880DF0D3FF050003D101E00120F5E7E3 +:105F1000FFDF6878800701D5022000E001202072B1 +:105F200099F800002328B2D9272159E719270E260E +:105F3000002C9DD0A088FDF785FA5FEA000900D10A +:105F4000FFDFC4F808A0A4F80CA084F808A0A07A89 +:105F500040F00300A07299F81E10C90961F3820095 +:105F6000A07299F81F2099F81E1012EAD11F05D0CF +:105F700099F8201001F01F0110292BD020F0080003 +:105F8000A07299F81F10607A61F3C3006072697A99 +:105F900001F003010129A2D140F00400607299F8D8 +:105FA0001E0000F003000228E87A16D0217B60F37F +:105FB00000012173AA7A607B62F300006073EA7AC1 +:105FC000520862F341012173A97A490861F3410043 +:105FD00060735CE740F00800D2E7617B60F300018A +:105FE0006173AA7A207B62F300002073EA7A520878 +:105FF00062F341016173A97A490861F3410020739A +:1060000045E710B5FE4C30B10146102204F12000E6 +:1060100008F013FA012084F8300010BD10B50446D2 +:1060200000F0E9FDF64920461022BDE8104020317D +:1060300008F003BA70B5F24D06004FF0000413D01B +:10604000FBF707F908B110240CE00621304608F0F0 +:1060500071FA411C05D028665FF0010085F85C00EC +:1060600000E00724204670BD0020F7E7007810F01C +:106070000F0204D0012A05D0022A0CD110E0000939 +:1060800009D10AE00009012807D0022805D0032819 +:1060900003D0042801D00720704708700020704703 +:1060A0000620704705282AD2DFE800F003070F1703 +:1060B0001F00087820F0FF001EE0087820F00F0095 +:1060C000401C20F0F000103016E0087820F00F009F +:1060D000401C20F0F00020300EE0087820F00F0087 +:1060E000401C20F0F000303006E0087820F00F006F +:1060F000401C20F0F000403008700020704707205E +:1061000070472DE9F041804688B00D4600270846CB +:10611000FBF7ECF8A8B94046FDF794F9040003D06A +:106120002078222815D104E043F2020008B0BDE82F +:10613000F08145B9A07F410603D500F00300022895 +:1061400001D01020F2E7A07FC10601D4010702D5DB +:106150000DB10820EAE7E17F090701D50D20E5E749 +:1061600000F0030002280DD165B12846FEF75EFF5E +:106170000700DBD1FBF736FB20B9E878800701D5B3 +:106180000620D3E7A07F00F00300022808D00020FB +:1061900080F0010140460DF089FE060002D00FE0BC +:1061A0000120F5E7A07F00F0030002280ED00020B8 +:1061B00080F00101002240460DF06FFE060007D07E +:1061C000A07F00F00300022804D009E00120EFE7DF +:1061D0000420ABE725B12A4631462046FEF74AFFA8 +:1061E0006946304600F017FD009800B9FFDF0099BE +:1061F000022006F1E0024870C1F824804A610022C2 +:106200000A81A27F02F00302022A1CD00120087139 +:10621000287800F00102087E62F3010008762A78EF +:10622000520862F3820008762A78920862F3C3006B +:1062300008762A78D20862F30410087624212046D2 +:10624000FCF768FF33E035B30871301D88613078A2 +:10625000400908777078C0F340004877287800F04C +:106260000102887F62F301008877A27FD20962F37E +:1062700082008877E27F62F3C3008877727862F3E6 +:1062800004108877A878C87701F1210228462031C8 +:10629000FEF711FF03E00320087105200876252191 +:1062A0002046FCF737FFA07F20F04000A07701A92F +:1062B00000980FF0F4FA022801D000B1FFDF384651 +:1062C00034E72DE9FF4F8DB09A4693460D460027DF +:1062D0000D98FDF7B7F8060006D03078262806D0CE +:1062E000082011B0BDE8F08F43F20200F9E7B07F5B +:1062F00000F00309B9F1020F11D04DB95846FEF76D +:1063000095FE0028EDD1B07F00F00300022806D0F2 +:10631000BBF1000F11D0FBF765FA20B10DE0BBF126 +:10632000000F50D109E006200DF068FD28B19BF860 +:106330000300800701D50620D3E7B07F00F00300FB +:10634000022809D05FF0000080F001010D980DF0E7 +:10635000ADFD040003D101E00120F5E7FFDF852D4D +:1063600027D007DCEDB1812D1DD0822D1DD0832DCE +:1063700008D11CE0862D1ED0882D1ED0892D1ED060 +:106380008A2D1ED00F2020710F281CD003F02EF96B +:10639000D8B101208DF81400201D06902079B0B1ED +:1063A00056E10020EFE70120EDE70220EBE70320B4 +:1063B000E9E70520E7E70620E5E70820E3E709200D +:1063C000E1E70A20DFE707208BE7112089E7B9F131 +:1063D000020F03D0A56F03D1A06F02E0656FFAE74B +:1063E000606F804631D04FF0010001904FF0020005 +:1063F00000905A4621463046FEF73CFE02E000007F +:10640000300200209BF8000000F00101A87861F341 +:106410000100A870B17FC90961F38200A870F17F03 +:1064200061F3C300A870617861F30410A87020784C +:10643000400928706078C0F3400068709BF8020043 +:10644000E87000206871287103E0022001900120AB +:106450000090A87898F80210C0F3C000C1F3C00102 +:10646000084003902CD05046FAF7F3FEC0BBDAF890 +:106470000C00FAF7EEFE98BBDAF81C00FAF7E9FE1A +:1064800070BBDAF80C00A060DAF81C00E0606078FD +:1064900098F8012042EA500161F34100607098F8D9 +:1064A0000210C0B200EA111161F300006070002018 +:1064B0002077009906F11700022907D0012106E094 +:1064C000607898F8012002EA5001E5E7002104EB2A +:1064D000810148610199701C022902D0012101E06B +:1064E00028E0002104EB81014861A87800F0030056 +:1064F000012857D198F8020000F00300012851D17B +:10650000B9F1020F04D02A1D691D5846FEF7D3FDCC +:10651000287998F8041008408DF82C00697998F8CB +:10652000052011408DF8301008433BD05046FAF753 +:1065300090FE08B11020D4E60AF110018B46B9F1A3 +:10654000020F17D00846002104F18C03CDE90003A7 +:1065500004F5AE7202920BAB2046039AFEF7F4FDEF +:106560000028E8D1B9F1020F08D0504608D14FF009 +:10657000010107E050464FF00101E5E75846F5E715 +:106580004FF0000104F1A403CDE9000304F5B0725B +:10659000029281F001010CAB2046039AFEF7D4FD74 +:1065A0000028C8D16078800733D4A87898F8021002 +:1065B000C0F38000C1F3800108432AD0297898F8FD +:1065C0000000F94AB9F1020F06D032F81120430059 +:1065D000DA4002F003070AE032F810204B00DA40FC +:1065E00012F0030705D0012F0AD0022F0AD0032F83 +:1065F00006D0039A6AB1012906D0042904D008E024 +:106600000227F6E70127F4E7012801D0042800D18A +:106610000427B07F40F08000B077F17F039860F3EB +:106620000001F1776078800705D50320A0710398F9 +:1066300070B9002029E00220022F18D0012F18D0B5 +:10664000042F2AD00020A071B07F20F08000B07706 +:1066500025213046FCF75EFD05A904F1E0000FF0AE +:1066600003F910B1022800D0FFDF002039E6A07145 +:10667000DFE7A0710D22002104F1200007F007FFE1 +:10668000207840F00200207001208DF8100004AA4C +:1066900031460D9800F098FADAE70120A071D7E7AB +:1066A0002DE9F04387B09046894604460025FCF763 +:1066B000C9FE060006D03078272806D0082007B08B +:1066C000BDE8F08343F20200F9E7B07F00F0030079 +:1066D000022809D05FF0000080F0010120460DF093 +:1066E000E5FB040003D101E00120F5E7FFDFA77916 +:1066F0005FEA090005D0012821D0B9F1020F26D1A7 +:1067000010E0B8F1000F22D1012F05D0022F05D0E3 +:10671000032F05D0FFDF2EE00C252CE001252AE019 +:10672000022528E04046FAF794FDB0B9032F0ED1B8 +:106730001022414604F11D0007F07FFE1BE0012FEF +:1067400002D0022F03D104E0B8F1000F13D00720CC +:10675000B5E74046FAF77DFD08B11020AFE71022FB +:10676000002104F11D0007F092FE0621404607F0CB +:10677000E1FEC4F81D002078252140F002002070C1 +:106780003046FCF7C7FC2078C10713D020F0010089 +:10679000207002208DF8000004F11D0002908DF899 +:1067A00004506946C3300FF05FF8022803D010B1DF +:1067B000FFDF00E02577002081E730B587B00D4688 +:1067C0000446FCF73FFE98B1807F00F003000228EA +:1067D00011D0002080F0010120460DF067FB04007D +:1067E0000ED02846FAF735FD38B1102007B030BD7D +:1067F00043F20200FAE70120ECE72078400701D4D9 +:106800000820F3E7294604F13D002022054607F061 +:1068100014FE207840F01000207001070FD520F002 +:106820000800207007208DF80000694604F1E000A0 +:1068300001950FF019F8022801D000B1FFDF002008 +:10684000D4E770B50D460646FCF7FCFD18B101789B +:10685000272921D102E043F2020070BD807F00F0C1 +:106860000300022808D0002080F0010130460DF01E +:106870001DFB040003D101E00120F5E7FFDFA07953 +:10688000022809D16078C00706D02A462146304642 +:10689000FEF7EBFC10B10FE0082070BDB4F860000B +:1068A0000E280BD204F1620102231022081F0DF002 +:1068B00084F9012101704570002070BD112070BD68 +:1068C00070B5064614460D460846FAF7C2FC18B9DC +:1068D0002046FAF7E4FC08B1102070BDA6F57F4011 +:1068E000FF380ED03046FCF7ADFD38B14178224676 +:1068F0004B08811C1846FCF774FD07E043F20200C8 +:1069000070BD2046FDF7A5FD0028F9D11021E01D3E +:1069100010F0EDFDE21D294604F1170000F08BF99F +:10692000002070BD2DE9F04104468AB01546884626 +:1069300000270846FAF7DAFC18B92846FAF7D6FC19 +:1069400018B110200AB0BDE8F0812046FCF77AFDAE +:10695000060003D0307827281BD102E043F2020062 +:10696000F0E7B07F00F00300022809D05FF00000DC +:1069700080F0010120460DF099FA040003D101E0F6 +:106980000120F5E7FFDF2078400702D56078800717 +:1069900001D40820D6E7B07F00F00300022805D01C +:1069A000A06F05D1A16F04E01C610200606FF8E7E1 +:1069B000616F407800B19DB1487810B1B8F1000F17 +:1069C0000ED0ADB1EA1D06A8E16800F034F910223E +:1069D00006A905F1170007F003FD18B1042707E029 +:1069E0000720AFE71022E91D04F12D0007F025FD77 +:1069F000B8F1000F06D0102208F1070104F11D00C4 +:106A000007F01BFD2078252140F002002070304661 +:106A1000FCF780FB2078C10715D020F00100207022 +:106A200002208DF8000004F11D0002901030039048 +:106A30008DF804706946B3300EF016FF022803D0BB +:106A400010B1FFDF00E0277700207BE7F8B515469F +:106A50000E460746FCF7F6FC040004D020782228F6 +:106A600004D00820F8BD43F20200F8BDA07F00F07A +:106A70000300022802D043F20500F8BD3046FAF7C1 +:106A8000E8FB18B92846FAF7E4FB08B11020F8BD76 +:106A900000953288B31C21463846FEF709FC1128C0 +:106AA00015D00028F3D1297C4A08A17F62F3C711D1 +:106AB000A177297CE27F61F30002E277297C8908D3 +:106AC00084F82010A17F21F04001A177F8BDA17FBB +:106AD0000907FBD4D6F80200C4F83600D6F8060041 +:106AE000C4F83A003088A0861022294604F1240018 +:106AF00007F0A3FC287C4108E07F61F34100E077C8 +:106B0000297C61F38200E077287C800884F82100EA +:106B1000A07F40F00800A0770020D3E770B50D46B5 +:106B200006460BB1072070BDFCF78CFC040007D0B3 +:106B30002078222802D3A07F800604D4082070BDCC +:106B400043F2020070BDADB1294630460CF076F834 +:106B500002F069FB297C4A08A17F62F3C711A17783 +:106B6000297CE27F61F30002E277297C890884F8BE +:106B7000201004E030460CF084F802F054FBA17FB2 +:106B800021F02001A17770BD70B50D46FCF75AFCCD +:106B9000040005D02846FAF782FB20B1102070BD12 +:106BA00043F2020070BD29462046FEF72FFB00206D +:106BB00070BD04E010F8012B0AB100207047491E97 +:106BC00089B2F7D20120704770B51546064602F02B +:106BD0000DFD040000D1FFDF207820F00F00801CA5 +:106BE00020F0F0002030207066802868A060BDE8AA +:106BF000704002F0FEBC10B5134C94F83000002831 +:106C000008D104F12001A1F110000EF06FFE012067 +:106C100084F8300010BD10B190F8B9202AB10A48AC +:106C200090F8350018B1002003E0B83001E00648C4 +:106C300034300860704708B50023009313460A46B5 +:106C40000DF031FB08BD00003002002018B1817842 +:106C5000012938D101E010207047018842F6011265 +:106C6000881A914231D018DC42F60102A1EB0200F1 +:106C700091422AD00CDC41B3B1F5C05F25D06FF44E +:106C8000C050081821D0A0F57060FF381BD11CE05F +:106C900001281AD002280AD117E0B0F5807F14D05D +:106CA00008DC012811D002280FD003280DD0FF28BE +:106CB00009D10AE0B0F5817F07D0A0F580700338D4 +:106CC00003D0012801D0002070470F2070470A2808 +:106CD0001FD008DC0A2818D2DFE800F0191B1F1F9C +:106CE000171F231D1F21102815D008DC0B2812D0D8 +:106CF0000C2810D00D2816D00F2806D10DE0112831 +:106D00000BD084280BD087280FD003207047002099 +:106D1000704705207047072070470F2070470420F8 +:106D20007047062070470C20704743F202007047FE +:106D300038B50C46050041D06946FFF797F90028A1 +:106D400019D19DF80010607861F302006070694607 +:106D5000681CFFF78BF900280DD19DF800106078B2 +:106D600061F3C5006070A978C1F34101012903D026 +:106D7000022905D0072038BD217821F0200102E04A +:106D8000217841F020012170410704D0A978C90879 +:106D900061F386106070607810F0380F07D0A97822 +:106DA000090961F3C710607010F0380F02D16078E4 +:106DB000400603D5207840F040002070002038BD08 +:106DC00070B504460020088015466068FFF7B0FFE4 +:106DD000002816D12089A189884211D8606880785E +:106DE000C0070AD0B1F5007F0AD840F20120B1FBFC +:106DF000F0F200FB1210288007E0B1F5FF7F01D907 +:106E00000C2070BD01F201212980002070BD10B559 +:106E10000478137864F3000313700478640864F34F +:106E2000410313700478A40864F382031370047898 +:106E3000E40864F3C30313700478240964F30413AF +:106E400013700478640964F34513137000788009A3 +:106E500060F38613137031B10878C10701D1800740 +:106E600001D5012000E0002060F3C713137010BDAE +:106E70004278530702D002F0070306E012F0380F01 +:106E800002D0C2F3C20300E001234A7863F3020296 +:106E90004A70407810F0380F02D0C0F3C20005E00D +:106EA000430702D000F0070000E0012060F3C502B4 +:106EB0004A7070472DE9F04F95B00D00824613D00F +:106EC00012220021284607F0E2FA4FF6FF7B05AABE +:106ED0000121584607F0A3F80024264637464FF410 +:106EE00020586FF4205972E0102015B0BDE8F08FE3 +:106EF0009DF81E0001280AD1BDF81C1041450BD099 +:106F000011EB09000AD001280CD002280CD0042C67 +:106F10000ED0052C0FD10DE0012400E00224BDF8B5 +:106F20001A6008E0032406E00424BDF81A7002E0A9 +:106F3000052400E00624BDF81A10514547D12C74F1 +:106F4000BEB34FF0000810AA4FF0070ACDE9028245 +:106F5000CDE900A80DF13C091023CDF81090424670 +:106F60003146584607F02BF908BBBDF83C002A46CD +:106F7000C0B210A90EF045FDC8B9AE81CFB1CDE9C0 +:106F800000A80DF1080C0AAE40468CE8410213231C +:106F900000223946584607F012F940B9BDF83C00C6 +:106FA000F11CC01EC0B22A1D0EF02BFD10B1032033 +:106FB0009BE70AE0BDF82900E881062C05D19DF881 +:106FC0001E00A872BDF81C00288100208DE705A8CE +:106FD00007F031F800288BD0FFF779FE85E72DE91F +:106FE000F0471C46DDE90978DDF8209015460E00D3 +:106FF000824600D1FFDF0CB1208818B1D5B1112035 +:10700000BDE8F087022D01D0012100E0002106F14A +:10701000140005F0CDFEA8F8000002463B462946C4 +:10702000504603F092F9C9F8000008B9A41C3C606E +:107030000020E5E71320E3E7F0B41446DDE904524D +:107040008DB1002314B1022C09D101E0012306E027 +:107050000D7CEE0703D025F0010501230D742146B8 +:10706000F0BC04F050BA1A80F0BC70472DE9FE4F16 +:1070700091461A881C468A468046FAB102AB4946B8 +:1070800003F063F9050019D04046A61C27880DF0CF +:1070900050F83246072629463B4600960CF05FFC26 +:1070A00020882346CDE900504A4651464046FFF726 +:1070B000C3FF002020800120BDE8FE8F0020FBE7F9 +:1070C0002DE9F04786B082460EA8904690E8B000C1 +:1070D000894604AA05A903A88DE807001E462A468A +:1070E00021465046FFF77BFF039901B1012139701A +:1070F000002818D1FA4904F1140204AB086003987F +:1071000005998DE8070042464946504606F003FAC5 +:10711000A8B1092811D2DFE800F005080510100A0F +:107120000C0C0E00002006B06AE71120FBE70720D8 +:10713000F9E70820F7E70D20F5E70320F3E7BDF8AE +:1071400010100398CDE9000133462A4621465046E7 +:10715000FFF772FFE6E72DE9F04389B01646DDE957 +:1071600010870D4681461C461422002103A807F013 +:107170008EF9012002218DF810108DF80C008DF889 +:107180001170ADF8146064B1A278D20709D08DF8FF +:107190001600E088ADF81A00A088ADF81800A068C5 +:1071A000079008A80095CDE90110424603A948467A +:1071B0006B68FFF785FF09B0BDE8F083F0B58BB0D1 +:1071C00000240646069407940727089405A8099406 +:1071D000019400970294CDE903400D461023224606 +:1071E000304606F0ECFF78B90AA806A9019400978A +:1071F0000294CDE90310BDF8143000222946304630 +:1072000006F07BFD002801D0FFF761FD0BB0F0BD5B +:1072100006F00CBC2DE9FC410C468046002602F02D +:10722000E5F9054620780D287ED2DFE800F0BC079E +:1072300013B325BD49496383AF959B00A8480068F7 +:1072400020B1417841F010014170ADE0404602F0BC +:10725000FDF9A9E0042140460CF028FE070000D10A +:10726000FFDF07F11401404605F037FDA5BB1321F0 +:107270004046FDF7CFFB97E0042140460CF016FE98 +:10728000070000D1FFDFE088ADF800000020B881E2 +:107290009DF80000010704D5C00602D5A088B8817A +:1072A00005E09DF8010040067ED5A088F88105B96B +:1072B000FFDF22462946404601F0ACFC022673E07F +:1072C000E188ADF800109DF8011009060FD50728D8 +:1072D00003D006280AD00AE024E0042140460CF03E +:1072E000E5FD060000D1FFDFA088F0810226CDB9C0 +:1072F000FFDF17E0042140460CF0D8FD070000D165 +:10730000FFDF07F1140006F0C8FB90F0010F02D177 +:10731000E079000648D5387C022640F00200387437 +:1073200005B9FFDF224600E03DE02946404601F076 +:1073300071FC39E0042140460CF0B8FD017C002DC1 +:1073400001F00206C1F340016171017C21F00201EC +:107350000174E7D1FFDFE5E702260121404602F094 +:10736000A7F921E0042140460CF0A0FD0546606825 +:1073700000902089ADF8040001226946404602F0E1 +:10738000B8F9287C20F0020028740DE0002DC9D146 +:10739000FFDFC7E7022600214046FBF799F8002DE2 +:1073A000C0D1FFDFBEE7FFDF3046BDE8FC813EB560 +:1073B0000C0009D001466B4601AA002006F084FFAC +:1073C00020B1FFF784FC3EBD10203EBD0020208090 +:1073D000A0709DF8050002A900F00700FEF762FE0C +:1073E00050B99DF8080020709DF8050002A9C0F36F +:1073F000C200FEF757FE08B103203EBD9DF808000D +:1074000060709DF80500C109A07861F30410A070B8 +:107410009DF80510890961F3C300A0709DF8041060 +:10742000890601D5022100E0012161F342009DF8A7 +:10743000001061F30000A07000203EBD70B514463E +:1074400006460D4651EA040005D075B10846F9F725 +:1074500044FF78B901E0072070BD2946304606F0A8 +:107460009AFF10B1BDE8704031E454B12046F9F7FD +:1074700034FF08B1102070BD21463046BDE8704091 +:1074800095E7002070BD2DE9FC5F0C46904605464F +:10749000002701780822007A3E46B2EB111F7DD109 +:1074A00004F10A0100910A31821E4FF0020A04F130 +:1074B000080B0191092A72D2DFE802F0EDE005F530 +:1074C00028287BAACE00688804210CF0EFFC060077 +:1074D00000D1FFDFB08928B152270726C3E00000A2 +:1074E0009402002051271026002C7DD06888A080AF +:1074F0000120A071A88900220099FFF79FFF0028B2 +:1075000073D1A8892081288AE081D1E0B5F8129052 +:10751000072824D1E87B000621D5512709F1140062 +:1075200086B2002CE1D0A88900220099FFF786FFDF +:1075300000285AD16888A08084F806A0A8892081F4 +:107540000120A073288A2082A4F81290A88A0090B3 +:1075500068884B46A969019A01F038FBA8E05027DA +:1075600009F1120086B2002C3ED0A88900225946AB +:10757000FFF764FF002838D16888A080A889E080E0 +:10758000287A072813D002202073288AE081E87B1C +:10759000C0096073A4F81090A88A01E085E082E039 +:1075A000009068884B4604F11202A969D4E70120D3 +:1075B000EAE7B5F81290512709F1140086B2002CC1 +:1075C00066D0688804210CF071FC83466888A0802E +:1075D000A88900220099FFF731FF00286ED184F8B6 +:1075E00006A0A889208101E052E067E00420A07392 +:1075F000288A2082A4F81290A88A009068884B46B6 +:10760000A969019A01F0E2FAA989ABF80E104FE0DE +:107610006888FBF717FF0746688804210CF046FCD2 +:10762000064607B9FFDF06B9FFDF687BC00702D057 +:107630005127142601E0502712264CB36888A080F9 +:10764000502F06D084F806A0287B594601F0CEFAC8 +:107650002EE0287BA11DF9E7FE49A88949898142CE +:1076600005D1542706269CB16888A08020E05327C6 +:107670000BE06888A080A889E08019E06888042170 +:107680000CF014FC00B9FFDF55270826002CF0D1C0 +:10769000A8F8006011E056270726002CF8D068886B +:1076A000A080002013E0FFDF02E0012808D0FFDF08 +:1076B000A8F800600CB1278066800020BDE8FC9F20 +:1076C00057270726002CE3D06888A080687AA0712D +:1076D000EEE7401D20F0030009B14143091D01EB15 +:1076E0004000704713B5DB4A00201071009848B184 +:1076F000002468460CF0F7F9002C02D1D64A009914 +:1077000011601CBD01240020F4E770B50D4614463D +:10771000064686B05C220021284606F0B8FE04B971 +:10772000FFDFA0786874A2782188284601F089FAE2 +:107730000020A881E881228805F11401304605F077 +:10774000B0FA6A460121304606F069FC1AE000BF33 +:107750009DF80300000715D5BDF806103046FFF769 +:107760002DFD9DF80300BDF8061040F010008DF8C7 +:107770000300BDF80300ADF81400FF233046059A5E +:1077800006F0D1FD684606F056FC0028E0D006B0B1 +:1077900070BD10B50C4601F1140005F0BAFA0146AF +:1077A000627C2046BDE8104001F080BA30B5044646 +:1077B000A84891B04FF6FF75C18905AA284606F082 +:1077C0002EFC30E09DF81E00A0422AD001282AD1CC +:1077D000BDF81C00B0F5205F03D042F601018842DD +:1077E00021D1002002AB0AAA0CA9019083E807006E +:1077F00007200090BDF81A1010230022284606F03A +:10780000DEFC38B9BDF828000BAAC0B20CA90EF0F6 +:10781000F8F810B1032011B030BD9DF82E00A04241 +:1078200001D10020F7E705A806F005FC0028C9D023 +:107830000520F0E770B5054604210CF037FB040085 +:1078400000D1FFDF04F114010C46284605F045FA8B +:1078500021462846BDE8704005F046BA70B58AB0AA +:107860000C460646FBF7EEFD050014D028782228CA +:1078700027D30CB1A08890B101208DF80C00032013 +:107880008DF8100000208DF8110054B1A088ADF8DB +:107890001800206807E043F202000AB070BD09201A +:1078A000FBE7ADF818000590042130460CF0FEFA15 +:1078B000040000D1FFDF04F1140005F040FA0007D6 +:1078C00001D40820E9E701F091FE60B108A8022187 +:1078D0000094CDE9011095F8232003A93046636890 +:1078E000FFF7EEFBD9E71120D7E72DE9F04FB2F80B +:1078F00002A0834689B0154689465046FBF7A2FD93 +:107900000746042150460CF0D1FA0026044605969D +:107910004FF002080696ADF81C6007B9FFDF04B906 +:10792000FFDF4146504603F055FF50B907AA06A9AC +:1079300005A88DE807004246214650466368FFF7D8 +:107940004EFB444807AB0660DDE9051204F1140064 +:10795000CDF80090CDE90320CDE9013197F823203F +:10796000594650466B6805F033FA06000AD0022EDD +:1079700004D0032E14D0042E00D0FFDF09B030460F +:10798000BDE8F08FBDF81C000028F7D00599CDE9BF +:1079900000104246214650466368FFF74DFBEDE775 +:1079A000687840F008006870E8E710B50C46FFF70B +:1079B000BFF900280BD1607800F00701012905D13B +:1079C00010F0380F02D02078810601D5072010BDB5 +:1079D00040F0C8002070002010BD2DE9F04F99B094 +:1079E00004464FF000081B48ADF81C80ADF820801D +:1079F000ADF82480A0F80880ADF81480ADF81880A8 +:107A0000ADF82880ADF82C80007916460D46474623 +:107A1000012808D0022806D0032804D0042802D068 +:107A2000082019B0ACE72046F9F713FCF0BB284654 +:107A3000F9F70FFCD0BB6068F9F758FCB0BB606881 +:107A400068B160892189884202D8B1F5007F05D9E3 +:107A50000C20E6E7940200201800002080460EAAC1 +:107A600006A92846FFF7ACF90028DAD168688078C3 +:107A7000C0F34100022808D19DF8190010F0380F1A +:107A800003D02869F9F729FC80B905A92069FFF717 +:107A90004FF90028C5D1206950B1607880079DF862 +:107AA000150000F0380002D5F0B301E011E0D8BBBA +:107AB0009DF8140080060ED59DF8150010F0380FC3 +:107AC00003D06068F9F709FC18B96068F9F70EFC93 +:107AD00008B11020A5E70BA906A8FFF7C9F99DF882 +:107AE0002D000BA920F00700401C8DF82D006069C7 +:107AF000FFF75BFF002894D10AA9A069FFF718F9E6 +:107B000000288ED19DF8280080062BD4A06940B1B2 +:107B10009DF8290000F00701012923D110F0380F4A +:107B200020D0E06828B100E01CE00078D0B11C282B +:107B300018D20FAA611C2046FFF769F901213846C7 +:107B400061F30F2082468DF85210B94642F60300C9 +:107B50000F46ADF850000DF13F0218A928680DF04E +:107B600052FF08B107205CE79DF8600015A9CDF829 +:107B70000090C01CCDE9019100F0FF0B00230BF237 +:107B80000122514614A806F075F9E8BBBDF854006F +:107B90000C90FB482A8929690092CDE901106B8974 +:107BA000BDF838202868069906F064F9010077D1FD +:107BB00020784FF0020AC10601D4800616D58DF850 +:107BC000527042F60210ADF85000CDF80C9008A9A2 +:107BD00003AACDF800A0CDE90121002340F2032241 +:107BE00014A80B9906F046F9010059D1E4484D4616 +:107BF00008380089ADF83D000FA8CDE90290CDF816 +:107C00000490CDF8109000E00CE04FF007095B46BF +:107C10000022CDF80090BDF854104FF6FF7006F02A +:107C20006CF810B1FFF753F8FBE69DF83C00000636 +:107C300024D52946012060F30F218DF852704FF4AE +:107C400024500395ADF8500062789DF80C00002395 +:107C500062F300008DF80C006278CDF800A05208A5 +:107C600062F341008DF80C0003AACDE9012540F232 +:107C7000032214A806F0FEF8010011D1606880B359 +:107C80002069A0B905A906A8FFF7F2F86078800777 +:107C900007D49DF8150020F038008DF8150006E097 +:107CA00077E09DF8140040F040008DF814008DF846 +:107CB000527042F60110ADF85000208940F20121C7 +:107CC000B0FBF1F201FB1202606809ABCDF8008055 +:107CD000CDE90103002314A8059906F0CBF80100B3 +:107CE00057D12078C00728D00395A06950B90AA9B8 +:107CF00006A8FFF7BDF89DF8290020F00700401CFA +:107D00008DF829009DF8280007A940F040008DF863 +:107D100028008DF8527042F60310ADF8500003AA07 +:107D2000CDF800A0CDE90121002340F2032214A8E0 +:107D30000A9906F09FF801002BD1E06868B3294644 +:107D4000012060F30F218DF8527042F60410ADF857 +:107D50005000E068002302788DF8582040788DF8B4 +:107D60005900E06816AA4088ADF85A00E06800792A +:107D70008DF85C00E068C088ADF85D00CDF800903B +:107D8000CDE901254FF4027214A806F073F8010042 +:107D900003D00C9800F0B6FF43E679480321083879 +:107DA000017156B100893080BDF824007080BDF8A3 +:107DB0002000B080BDF81C00F080002031E670B5D6 +:107DC00001258AB016460B46012802D0022816D19A +:107DD00004E08DF80E504FF4205003E08DF80E5063 +:107DE00042F60100ADF80C005BB10024601C60F3AA +:107DF0000F2404AA08A918460DF005FE18B10720A3 +:107E00004BE5102049E504A99DF820205C48CDE908 +:107E10000021801E02900023214603A802F20122C5 +:107E200006F028F810B1FEF752FF36E5544808383E +:107E30000EB1C1883180057100202EE5F0B593B0F8 +:107E4000044601268DF83E6041F601000F46ADF86C +:107E50003C0011AA0FA93046FFF7B1FF002837D127 +:107E60002000474C4FF00005A4F1080432D01C223A +:107E7000002102A806F00BFB9DF808008DF83E607B +:107E800040F020008DF8080042F60520ADF83C00D7 +:107E900004200797ADF82C00ADF8300039480A905F +:107EA0000EA80D900E950FA80990ADF82E506A46B9 +:107EB00009A902A8FFF791FD002809D1BDF800002B +:107EC0006081BDF80400A081401CE0812571002084 +:107ED00013B0F0BD6581A581BDF84400F4E72DE93C +:107EE000F74F2749A0B00024083917940A79A14612 +:107EF000012A04D0022A02D0082023B040E5CA8813 +:107F0000824201D00620F8E721988A46824201D1B8 +:107F10000720F2E70120214660F30F21ADF8480069 +:107F20004FF6FF788DF86E000691ADF84A8042F664 +:107F3000020B8DF872401CA9ADF86CB0ADF8704022 +:107F40001391ADF8508012A806F0A4F800252E4633 +:107F50002F460DAB072212A9404606F09EF898B1B5 +:107F60000A2861D1B5B3AEB3ADF86450ADF8666020 +:107F70009DF85E008DF8144019AC012868D06FE0C0 +:107F80009C020020266102009DF83A001FB30128E0 +:107F900059D1BDF8381059451FD118A809A9019425 +:107FA0000294CDE9031007200090BDF8361010238D +:107FB0000022404606F003F9B0BBBDF8600004287B +:107FC00001D006284AD1BDF82410219881423AD127 +:107FD0000F2092E73AE0012835D1BDF83800B0F51E +:107FE000205F03D042F6010188422CD1BAF8060086 +:107FF000BDF83610884201D1012700E0002705B105 +:108000009EB1219881421ED118A809AA0194029418 +:10801000CDE90320072000900D46102300224046A2 +:1080200006F0CDF800B902E02DE04E460BE0BDF8B9 +:108030006000022801D0102810D1C0B217AA09A9E7 +:108040000DF0DFFC50B9BDF8369082E7052054E70B +:1080500005A917A8221D0DF0D6FC08B103204CE796 +:108060009DF814000023001DC2B28DF81420229840 +:108070000092CDE901401BA8069905F0FBFE10B95E +:1080800002228AF80420FEF722FE36E710B50B46DE +:10809000401E88B084B205AA00211846FEF7B7FE3C +:1080A00000200DF1080C06AA05A901908CE8070034 +:1080B000072000900123002221464FF6FF7005F0B3 +:1080C0001CFE0446BDF81800012800D0FFDF204642 +:1080D000FEF7FDFD08B010BDF0B5FF4F044687B0B8 +:1080E00038790E46032804D0042802D0082007B0AF +:1080F000F0BD04AA03A92046FEF762FE0500F6D1F2 +:1081000060688078C0F3410002280AD19DF80D0014 +:1081100010F0380F05D02069F9F7DFF808B110200A +:10812000E5E7208905AA21698DE807006389BDF884 +:1081300010202068039905F09DFE10B1FEF7C7FDE1 +:10814000D5E716B1BDF814003080042038712846F8 +:10815000CDE7F8B50C0006460BD001464FF6FF758B +:1081600000236A46284606F0AFF820B1FEF7AFFDBF +:10817000F8BD1020F8BD69462046FEF7D9FD00285D +:10818000F8D1A078314600F001032846009A06F0A5 +:10819000CAF8EBE730B587B0144600220DF1080CA1 +:1081A00005AD01928CE82C00072200920A46014698 +:1081B00023884FF6FF7005F0A0FDBDF81410218054 +:1081C000FEF785FD07B030BD70B50D4604210BF0FC +:1081D0006DFE040000D1FFDF294604F11400BDE864 +:1081E000704004F0A5BD70B50D4604210BF05EFE95 +:1081F000040000D1FFDF294604F11400BDE87040FF +:1082000004F0B9BD70B50D4604210BF04FFE04001B +:1082100000D1FFDF294604F11400BDE8704004F0EE +:10822000D1BD70B5054604210BF040FE040000D11D +:10823000FFDF214628462368BDE870400122FEF793 +:1082400015BF70B5064604210BF030FE040000D1C6 +:10825000FFDF04F1140004F05CFD401D20F0030575 +:1082600011E0011D00880022431821463046FEF728 +:10827000FDFE00280BD0607CABB2684382B2A068E0 +:10828000011D0BF0D0FCA06841880029E9D170BD28 +:1082900070B5054604210BF009FE040000D1FFDF94 +:1082A000214628466368BDE870400222FEF7DEBE24 +:1082B00070B50E46054601F099F9040000D1FFDFC4 +:1082C0000120207266726580207820F00F00001D6A +:1082D00020F0F00040302070BDE8704001F089B916 +:1082E00010B50446012900D0FFDF2046BDE810404C +:1082F0000121FAF7EDB82DE9F04F97B04FF0000AE1 +:108300000C008346ADF814A0D04619D0E06830B117 +:10831000A068A8B10188ADF81410A0F800A05846D4 +:10832000FBF790F8070043F2020961D03878222861 +:108330005CD3042158460BF0B9FD050005D103E0DC +:10834000102017B0BDE8F08FFFDF05F1140004F036 +:10835000E0FC401D20F00306A078012803D002288D +:1083600001D00720EDE7218807AA584605F057FEFF +:1083700030BB07A805F05FFE10BB07A805F05BFE49 +:1083800048B99DF82600012805D1BDF82400A0F5C4 +:108390002451023902D04FF45050D2E7E068B0B116 +:1083A000CDE902A00720009005AACDF804A0049210 +:1083B000A2882188BDF81430584605F09EFC10B103 +:1083C000FEF785FCBDE7A168BDF8140008809DF8A4 +:1083D0001F00C00602D543F20140B2E70B9838B146 +:1083E000A1780078012905D080071AD40820A8E7D1 +:1083F0004846A6E7C007F9D002208DF83C00A868DF +:108400004FF00009A0B1697C4288714391420FD9B5 +:108410008AB2B3B2011D0BF0BCFB8046A0F800A0ED +:1084200006E003208DF83C00D5F800804FF00109EC +:108430009DF8200010F0380F00D1FFDF9DF82000DC +:108440002649C0F3C200084497F8231010F8010C25 +:10845000884201D90F2074E72088ADF8400014A9A4 +:108460000095CDE90191434607220FA95846FEF732 +:1084700027FE002891D19DF8500050B9A07801281E +:1084800007D1687CB3B2704382B2A868011D0BF0BB +:1084900094FB002055E770B5064615460C46084685 +:1084A000FEF7D4FB002805D12A4621463046BDE818 +:1084B000704084E470BD12E570B51E4614460D0090 +:1084C0000ED06CB1616859B160B10349C98881426D +:1084D00008D0072070BD000094020020296102002E +:1084E0001020F7E72068FEF7B1FB0028F2D13246F2 +:1084F00021462846BDE87040FFF76FBA70B51546B3 +:108500000C0006D038B1FE490989814203D007200A +:10851000E0E71020DEE72068FEF798FB0028D9D1BD +:1085200029462046BDE87040D6E570B5064686B0BF +:108530000D4614461046F8F7B2FED0BB6068F8F757 +:10854000D5FEB0BBA6F57F40FF3803D03046FAF722 +:1085500079FF80B128466946FEF7ACFC00280CD1B3 +:108560009DF810100F2008293DD2DFE801F0080621 +:108570000606060A0A0843F2020006B0AAE703202C +:10858000FBE79DF80210012908D1BDF80010B1F5F4 +:10859000C05FF2D06FF4C052D142EED09DF8061009 +:1085A00001290DD1BDF80410A1F52851062907D2E3 +:1085B00000E029E0DFE801F0030304030303DCE744 +:1085C0009DF80A1001290FD1BDF80810B1F5245FFC +:1085D000D3D0A1F60211B1F50051CED00129CCD0F3 +:1085E000022901D1C9E7FFDF606878B9002305AA35 +:1085F0002946304605F068FE10B1FEF768FBBCE77F +:108600009DF81400800601D41020B6E76188224648 +:1086100028466368FFF7BEFDAFE72DE9F0438146CA +:1086200087B0884614461046F8F739FE18B1102076 +:1086300007B0BDE8F083002306AA4146484605F08E +:1086400043FE10B1FEF743FBF2E79DF81800C006A9 +:1086500002D543F20140EBE70025072705A8019565 +:1086600000970295CDE9035062884FF6FF734146AB +:10867000484605F0A4FD060013D16068F8F70FFE28 +:1086800060B960680195CDE9025000970495238890 +:1086900062884146484605F092FD0646BDF8140042 +:1086A00020803046CEE739B1954B0A889B899A42A3 +:1086B00002D843F2030070471DE610B586B0904C17 +:1086C0000423ADF81430638943B1A4898C4201D2EC +:1086D000914205D943F2030006B010BD0620FBE726 +:1086E000ADF81010002100910191ADF80030022189 +:1086F0008DF8021005A9029104A90391ADF812208A +:108700006946FFF7F8FDE7E72DE9FC4781460D468E +:108710000846F8F79EFD88BB4846FAF793FE5FEAE5 +:1087200000080AD098F80000222829D304214846DE +:108730000BF0BCFB070005D103E043F20200BDE8EB +:10874000FC87FFDF07F1140004F0F9FA06462878E9 +:10875000012803D0022804D00720F0E7B0070FD586 +:1087600002E016F01C0F0BD0A8792C1DC00709D011 +:10877000E08838B1A068F8F76CFD18B11020DEE78A +:108780000820DCE721882A780720B1F5847F35D0DE +:108790001EDC40F20315A1F20313A94226D00EDC21 +:1087A000B1F5807FCBD003DCF9B1012926D1C6E732 +:1087B000A1F58073013BC2D0012B1FD113E0012B27 +:1087C000BDD0022B1AD0032BB9D0042B16D112E046 +:1087D000A1F20912082A11D2DFE802F00B040410FA +:1087E00010101004ABE7022AA9D007E0012AA6D096 +:1087F00004E0320700E0F206002AA0DACDB200F071 +:10880000F5FE50B198F82300CDE90005FA8923461A +:1088100039464846FEF79FFC91E711208FE72DE986 +:10882000F04F8BB01F4615460C4683460026FAF7DC +:1088300009FE28B10078222805D208200BB081E576 +:1088400043F20200FAE7B80801D00720F6E7032F49 +:1088500000D100274FF6FF79CCB1022D71D320460D +:10886000F8F744FD30B904EB0508A8F10100F8F76A +:108870003DFD08B11020E1E7AD1E38F8028CAAB228 +:108880002146484605F081FE40455AD1ADB21C490B +:10889000B80702D58889401C00E001201FFA80F843 +:1088A000F80701D08F8900E04F4605AA4146584697 +:1088B00005F0B5FB4FF0070A4FF00009FCB1204668 +:1088C00008E0408810283CD8361D304486B2AE42BD +:1088D00037D2A01902884245F3D352E09DF8170021 +:1088E00002074ED57CB304EB0608361DB8F80230FB +:1088F000B6B2102B25D89A19AA4222D802E040E03D +:1089000094020020B8F8002091421AD1C0061BD56D +:10891000CDE900A90DF1080C0AAAA11948468CE876 +:108920000700B8F800100022584605F0E6F910B12B +:10893000FEF7CDF982E7B8F80200BDF828108842AA +:1089400002D00B207AE704E0B8F80200304486B287 +:1089500006E0C00604D55846FEF730FC00288AD150 +:108960009DF81700BDF81A1020F010008DF81700C0 +:10897000BDF81700ADF80000FF235846009A05F037 +:10898000D2FC05A805F057FB18B9BDF81A10B9427A +:10899000A4D9042158460BF089FA040000D1FFDF66 +:1089A000A2895AB1CDE900A94D4600232146584677 +:1089B000FEF7D1FB0028BDD1A5813FE700203DE7B0 +:1089C0002DE9FF4F8BB01E4617000D464FF00004F7 +:1089D00012D0B00802D007200FB0B3E4032E00D1AC +:1089E00000265DB10846F8F778FC28B93888691E7A +:1089F0000844F8F772FC08B11020EDE7C74AB00749 +:108A000001D5D18900E00121F0074FF6FF7802D0AF +:108A1000D089401E00E0404686B206AA0B9805F0B9 +:108A2000FEFA4FF000094FF0070B0DF1140A38E081 +:108A30009DF81B00000734D5CDF80490CDF800B0A8 +:108A4000CDF80890CDE9039A434600220B9805F033 +:108A5000B6FB60BB05B3BDF814103A882144281951 +:108A6000091D8A4230D3BDF81E2020F8022BBDF824 +:108A7000142020F8022BCDE900B9CDE90290CDF801 +:108A800010A0BDF81E10BDF8143000220B9805F0A0 +:108A900096FB08B103209FE7BDF814002044001D99 +:108AA00084B206A805F0C7FA20B10A2806D0FEF75E +:108AB0000EF991E7BDF81E10B142B9D934B17DB1BC +:108AC0003888A11C884203D20C2085E7052083E763 +:108AD00022462946404605F058FD014628190180E6 +:108AE000A41C3C80002077E710B50446F8F7D7FBBC +:108AF00008B1102010BD8948C0892080002010BD19 +:108B0000F0B58BB00D4606461422002103A805F0EF +:108B1000BEFC01208DF80C008DF8100000208DF8AF +:108B20001100ADF814503046FAF78CFC48B10078CB +:108B3000222812D3042130460BF0B8F9040005D1E5 +:108B400003E043F202000BB0F0BDFFDF04F11400BC +:108B5000074604F0F4F8800601D40820F3E7207CEF +:108B6000022140F00100207409A80094CDE9011011 +:108B7000072203A930466368FEF7A2FA20B1217CE0 +:108B800021F001012174DEE729463046F9F791FC16 +:108B900008A9384604F0C2F800B1FFDFBDF8204054 +:108BA000172C01D2172000E02046A84201D92C46FC +:108BB00002E0172C00D2172421463046FFF713FBA2 +:108BC00021463046F9F799F90020BCE7F8B51C4674 +:108BD00015460E46069F0BF09AFA2346FF1DBCB2BF +:108BE00031462A4600940AF086FEF8BD70B50C4660 +:108BF00005460E220021204605F049FC0020208079 +:108C00002DB1012D01D0FFDF64E4062000E0052036 +:108C1000A0715FE410B548800878134620F00F007B +:108C2000001D20F0F00080300C4608701422194618 +:108C300004F1080005F001FC00F0DBFC374804609B +:108C400010BD2DE9F047DFF8D890491D064621F008 +:108C5000030117460C46D9F800000AF062FF050030 +:108C600000D1FFDF4FF000083560A5F800802146F5 +:108C7000D9F800000AF055FF050000D1FFDF75604C +:108C8000A5F800807FB104FB07F1091D0BD0D9F8CE +:108C900000000AF046FF040000D1FFDFB460C4F812 +:108CA0000080BDE8F087C6F80880FAE72DE9F041BA +:108CB0001746491D21F00302194D06460168144666 +:108CC00028680AF059FF2246716828680AF054FFA4 +:108CD0003FB104FB07F2121D03D0B16828680AF007 +:108CE0004BFF04200BF08AF8044604200BF08EF8AA +:108CF000201A012804D12868BDE8F0410AF006BF17 +:108D0000BDE8F08110B50C4605F058F900B1FFDF61 +:108D10002046BDE81040FDF7DABF000094020020B5 +:108D20001800002038B50C468288817B19B1418932 +:108D3000914200D90A462280C188121D90B26A462B +:108D40000AF0B2F8BDF80000032800D30320C1B236 +:108D5000208801F020F838BD38B50C468288817B28 +:108D600019B10189914200D90A462280C188121D99 +:108D700090B26A460AF098F8BDF80000022800D3C5 +:108D80000220C1B2208801F006F8401CC0B238BDF4 +:108D90002DE9FF5F82468B46F74814460BF103022C +:108DA000D0E90110CDE9021022F0030201A84FF42E +:108DB000907101920AF097FEF04E002C02D1F0491A +:108DC000019A8A60019901440191B57F05F101057D +:108DD00004D1E8B20CF098FD00B1FFDF019800EB80 +:108DE0000510C01C20F0030101915CB9707AB27AC1 +:108DF0001044C2B200200870B08C80B204F03DFF75 +:108E000000B1FFDF0198716A08440190214601A872 +:108E100000F084FF80460198C01C20F00300019000 +:108E2000B37AF27A717A04B100200AF052FF019904 +:108E300008440190214601A800F0B8FFCF48002760 +:108E40003D4690F801900CE0284600F04AFF0646A7 +:108E500081788088F9F7E8F871786D1C00FB01775C +:108E6000EDB24D45F0D10198C01C20F003000190F7 +:108E700004B100203946F9F7E2F8019900270844C7 +:108E80000190BE483D4690F801900CE0284600F065 +:108E900028FF0646C1788088FEF71BFC71786D1CA0 +:108EA00000FB0177EDB24D45F0D10198C01C20F0D8 +:108EB0000300019004B100203946FEF713FC01992C +:108EC0004FF0000908440190AC484D4647780EE049 +:108ED000284600F006FF0646807B30B106F1080008 +:108EE00002F09CF9727800FB02996D1CEDB2BD4254 +:108EF000EED10198C01C20F00300019004B10020C5 +:108F00009F494A78494602F08DF901990844019039 +:108F1000214601A800F0B8FE0198C01D20F007000E +:108F20000190DAF80010814204D3A0EB0B01B1F5F7 +:108F3000803F04DB4FF00408CAF8000004E0CAF8E0 +:108F40000000B8F1000F03D0404604B0BDE8F09F28 +:108F500084BB8C490020019A0EF044FEFBF714FA02 +:108F6000864C207F0090607F012825D0002328B305 +:108F70000022824800211030F8F73AFA00B1FFDFF2 +:108F80007E49E07F2031FEF759FF00B1FFDF7B48CB +:108F90004FF4F6720021443005F079FA7748042145 +:108FA000443080F8E91180F8EA11062180F8EB11CD +:108FB000032101710020C8E70123D8E702AAD8E7FE +:108FC00070B56E4C06464434207804EB4015E078CA +:108FD000083598B9A01990F8E80100280FD0A078BA +:108FE0000F2800D3FFDF20220021284605F04FFA8A +:108FF000687866F3020068700120E070284670BD52 +:109000002DE9F04105460C460027007805219046E1 +:109010003E46B1EB101F00D0FFDF287A50B1012887 +:109020000ED0FFDFA8F800600CB12780668000201A +:10903000BDE8F0810127092674B16888A08008E0A6 +:109040000227142644B16888A0802869E060A88AB5 +:109050002082287B2072E5E7A8F80060E7E730B5BA +:10906000464C012000212070617020726072032242 +:10907000A272E07261772177217321740521218327 +:109080001F216183607440A161610A21A177E077AB +:1090900039483B4DB0F801102184C07884F8220093 +:1090A0004FF4B06060626868C11C21F00301814226 +:1090B00000D0FFDF6868606030BD30B5304C1568A7 +:1090C000636810339D4202D20420136030BD2B4BE5 +:1090D0005D785A6802EB0512107051700320D08041 +:1090E000172090800120D0709070002090735878E5 +:1090F000401C5870606810306060002030BD70B552 +:1091000006461E480024457807E0204600F0E9FDA9 +:109110000178B14204D0641CE4B2AC42F5D1002025 +:1091200070BDF7B5074608780C4610B3FFF7E7FFA8 +:109130000546A7F12006202F06D0052E19D2DFE81C +:1091400006F00F383815270000F0D6FD0DB169780C +:1091500000E00021401AA17880B20844FF2808D816 +:10916000A07830B1A088022831D202E060881728A8 +:109170002DD20720FEBD000030610200B0030020A8 +:109180001C000020000000206E52463578000000D0 +:10919000207AE0B161881729EBD3A1881729E8D399 +:1091A000A1790029E5D0E1790029E2D0402804D94D +:1091B000DFE7242F0BD1207A48B161884FF6FB708E +:1091C000814202D8A188814201D90420D2E765B941 +:1091D000207802AA0121FFF770FF0028CAD1207869 +:1091E000FFF78DFF050000D1FFDF052E18D2DFE865 +:1091F00006F0030B0E081100A0786870A088E880C4 +:109200000FE06088A8800CE0A078A87009E0A07842 +:10921000E87006E054F8020FA8606068E86000E0BB +:10922000FFDF0020A6E71A2835D00DDC132832D244 +:10923000DFE800F01B31203131272723252D313184 +:1092400029313131312F0F00302802D003DC1E28A4 +:1092500021D1072070473A3809281CD2DFE800F0F6 +:10926000151B0F1B1B1B1B1B07000020704743F225 +:109270000400704743F202007047042070470D203D +:1092800070470F2070470820704711207047132047 +:109290007047062070470320704710B5007800F033 +:1092A000010009F0F3FDBDE81040BCE710B50078FF +:1092B00000F0010009F0F3FDBDE81040B3E70EB582 +:1092C000017801F001018DF80010417801F00101F1 +:1092D0008DF801100178C1F340018DF8021041783A +:1092E000C1F340018DF80310017889088DF804104E +:1092F000417889088DF8051081788DF80610C178BD +:109300008DF8071000798DF80800684608F0FDFD1B +:10931000FFF789FF0EBD2DE9FC5FDFF8F883FE4CF7 +:1093200000264FF490771FE0012000F082FD01201D +:10933000FFF746FE05463946D8F808000AF0F1FB6B +:10934000686000B9FFDF686808F0AAFCB0B1284681 +:10935000FAF75EFB284600F072FD28B93A466968C4 +:10936000D8F808000AF008FC94F9E9010428DBDACF +:1093700002200AF043FD07460025AAE03A46696844 +:10938000D8F808000AF0F8FBF2E7B8F802104046F7 +:10939000491C89B2A8F80210B94201D300214180CA +:1093A0000221B8F802000AF081FD002866D0B8F862 +:1093B0000200694609F0CFFCFFF735FF00B1FFDF7F +:1093C0009DF80000019078B1B8F802000AF0B1FEF3 +:1093D0005FEA000900D1FFDF48460AF020F918B122 +:1093E000B8F8020002F0E4F9B8F802000AF08FFEC3 +:1093F0005FEA000900D1FFDF48460AF008F9E8BB40 +:109400000321B8F802000AF051FD5FEA000B4BD1CE +:10941000FFDF49E0DBF8100010B10078FF284DD0E5 +:10942000022000F006FD0220FFF7CAFD82464846F2 +:109430000AF0F9F9CAF8040000B9FFDFDAF804000D +:109440000AF0C1FA002100900170B8F802105046ED +:10945000AAF8021002F0B2F848460AF0B6FA00B9CB +:10946000FFDF019800B10126504600F0E8FC18B972 +:109470009AF80100000705D5009800E027E0CBF836 +:10948000100011E0DBF8101039B10878401C10F022 +:10949000FF00087008D1FFDF06E0002211464846B1 +:1094A00000F0F0FB00B9FFDF94F9EA01022805DBC8 +:1094B000B8F8020002F049F80028ABD194F9E901AC +:1094C000042804DB48460AF0E4FA00B101266D1CCA +:1094D000EDB2BD4204D294F9EA010228BFF655AFBD +:1094E000002E7FF41DAFBDE8FC5F032000F0A1BC9F +:1094F00010B5884CE06008682061AFF2E510F9F71C +:10950000E4FC607010BD844800214438017081483B +:10951000017082494160704770B505464FF0805038 +:109520000C46D0F8A410491C05D1D0F8A810C943A6 +:109530000904090C0BD050F8A01F01F0010129709B +:10954000416821608068A080287830B970BD06210C +:1095500020460DF0CCFF01202870607940F0C0005B +:10956000607170BD70B54FF080540D46D4F8801016 +:10957000491C0BD1D4F88410491C07D1D4F88810A9 +:10958000491C03D1D4F88C10491C0CD0D4F880109D +:109590000160D4F884104160D4F888108160D4F858 +:1095A0008C10C16002E010210DF0A1FFD4F89000F2 +:1095B000401C0BD1D4F89400401C07D1D4F898007B +:1095C000401C03D1D4F89C00401C09D054F8900FE3 +:1095D000286060686860A068A860E068E86070BDA6 +:1095E0002846BDE8704010210DF081BF4A4800793F +:1095F000E6E470B5484CE07830B3207804EB4010D6 +:10960000407A00F00700204490F9E801002800DCCF +:10961000FFDF2078002504EB4010407A00F00700BF +:10962000011991F8E801401E81F8E8012078401CFA +:10963000C0B220700F2800D12570A078401CA07007 +:109640000DF0D4FDE57070BDFFDF70BD3EB5054681 +:1096500003210AF02BFC044628460AF058FD054673 +:1096600004B9FFDF206918B10078FF2800D1FFDFBF +:1096700001AA6946284600F005FB60B9FFDF0AE051 +:10968000002202A9284600F0FDFA00B9FFDF9DF88C +:10969000080000B1FFDF9DF80000411E8DF80010AA +:1096A000EED220690199884201D1002020613EBD9F +:1096B00070B50546A0F57F400C46FF3800D1FFDFAE +:1096C000012C01D0FFDF70BDFFF790FF040000D137 +:1096D000FFDF207820F00F00401D20F0F000503018 +:1096E000207065800020207201202073BDE870404A +:1096F0007FE72DE9F04116460D460746FFF776FF56 +:10970000040000D1FFDF207820F00F00401D20F082 +:10971000F00005E01C000020F403002048140020A5 +:109720005030207067800120207228682061A8884E +:10973000A0822673BDE8F0415BE77FB5FFF7DFFC51 +:10974000040000D1FFDF02A92046FFF7EBFA05462F +:1097500003A92046FFF700FB8DF800508DF80100AB +:10976000BDF80800001DADF80200BDF80C00001D9A +:10977000ADF80400E088ADF80600684609F070FB1B +:10978000002800D0FFDF7FBD2DE9F05FFC4E814651 +:10979000307810B10820BDE8F09F4846F7F77FFD0C +:1097A00008B11020F7E7F74C207808B9FFF757FC0D +:1097B000A17A607A4D460844C4B200F09DFAA042F6 +:1097C00007D2201AC1B22A460020FFF776FC0028F3 +:1097D000E1D17168EB48C91C002721F003017160D9 +:1097E000B3463E463D46BA463C4690F801800AE004 +:1097F000204600F076FA4178807B0E4410FB01553C +:10980000641CE4B27F1C4445F2D10AEB870000EBF4 +:10981000C600DC4E00EB85005C46F17A012200EBCD +:109820008100DBF80410451829464846FFF7B0FAD6 +:10983000070012D00020FFF762FC05000BD005F1F5 +:109840001300616820F00300884200D0FFDF7078C9 +:10985000401E7070656038469DE7002229464846E4 +:10986000FFF796FA00B1FFDFD9F8000060604FF60D +:10987000FF7060800120207000208CE72DE9F0410E +:109880000446BF4817460D46007810B10820BDE8D1 +:10989000F0810846F7F7DDFC08B11020F7E7B94E74 +:1098A000307808B9FFF7DBFB601E1E2807D8012CB3 +:1098B00023D12878FE2820D8B0770020E7E7A4F14C +:1098C00020001F2805D8E0B23A462946BDE8F041FD +:1098D00027E4A4F140001F2805D829462046BDE80A +:1098E000F04100F0D4BAA4F1A0001F2805D8294601 +:1098F0002046BDE8F04100F006BB0720C7E72DE990 +:10990000F05F81460F460846F7F7C9FC48B948465C +:10991000F7F7E3FC28B909F1030020F003014945FA +:1099200001D0102037E797484FF0000B4430817882 +:1099300069B14178804600EB411408343E883A46CC +:109940000021204600F089FA050004D027E0A7F89E +:1099500000B005201FE7B9F1000F24D03888B042CD +:1099600001D90C251FE0607800F00700824600F066 +:1099700060FA08EB0A063A4696F8E8014946401CA8 +:1099800086F8E801204600F068FA054696F8E801F6 +:10999000401E86F8E801032000F04BFA2DB10C2D93 +:1099A00001D0A7F800B02846F5E6754F5046BAF149 +:1099B000010F25D002280DD0BAF1030F35D0FFDFFB +:1099C00098F801104046491CC9B288F801100F29C7 +:1099D00037D038E0606828B16078000702D460882A +:1099E000FFF734FE98F8EA014446012802D178785E +:1099F000F9F78AFA94F9EA010428E1DBFFDFDFE7EF +:109A0000616821B14FF49072B8680AF0B5F898F81F +:109A1000E9014446032802D17878F9F775FA94F9F8 +:109A2000E9010428CCDBFFDFCAE76078C00602D575 +:109A30006088FFF70BFE98F9EB010628C0DBFFDF1B +:109A4000BEE780F801B08178491E88F8021096F8C8 +:109A5000E801401C86F8E801A5E770B50C4605460C +:109A6000F7F7F7FB18B92046F7F719FC08B11020F3 +:109A700070BD28460BF07FFF207008B1002070BD3C +:109A8000042070BD70B505460BF08EFFC4B22846A9 +:109A9000F7F723FC08B1102070BD35B128782C7081 +:109AA00018B1A04201D0072070BD2046FDF77EFE10 +:109AB000052805D10BF07BFF012801D0002070BDE7 +:109AC0000F2070BD70B5044615460E460846F7F7E0 +:109AD000C0FB18B92846F7F7E2FB08B1102070BDAB +:109AE000022C03D0102C01D0092070BD2A4631462B +:109AF00020460BF086FF0028F7D0052070BD70B51A +:109B000014460D460646F7F7A4FB38B92846F7F782 +:109B1000C6FB18B92046F7F7E0FB08B1102070BD6E +:109B20002246294630460BF06EFF0028F7D007206A +:109B300070BD3EB50446F7F7B2FB08B110203EBD3C +:109B4000684608F053F9FFF76EFB0028F7D19DF83F +:109B500006002070BDF808006080BDF80A00A080F3 +:109B600000203EBD70B505460C460846F7F7B5FB2C +:109B700020B95CB12068F7F792FB28B1102070BDC6 +:109B80001C000020B0030020A08828B121462846F0 +:109B9000BDE87040FDF762BE0920F0E770B50546EC +:109BA0000C460846F7F755FBA0BB681E1E280ED8CA +:109BB000032D01D90720E2E705B9FFDFFE4800EBDE +:109BC000850050F8041C2046BDE870400847A5F108 +:109BD00020001F2805D821462846BDE87040FAF726 +:109BE00042BBA5F160001F2805D821462846BDE8E4 +:109BF0007040F8F7DABCF02D0DD0F12D15D0BF2D47 +:109C0000D8D1A078218800F0010001F08DFB98B137 +:109C10000020B4E703E0A068F7F71BFB08B11020B1 +:109C2000ADE7204609F081F902E0207809F0A0F9BB +:109C3000BDE87040FFF7F7BA0820A0E770B504460A +:109C40000D460846F7F72BFB30B9601E1E280FD8CB +:109C50002846F7F7FEFA08B1102090E7012C03D050 +:109C6000022C01D0032C01D1062088E7072086E7CB +:109C7000A4F120001F28F9D829462046BDE87040ED +:109C8000FAF762BB09F092BC38B50446CB48007BBA +:109C900000F00105F9B904F01DFC0DB1226800E0E7 +:109CA0000022C7484178C06807F06DFDC4481030F5 +:109CB000C0788DF8000010B1012802D004E0012026 +:109CC00000E000208DF80000684608F0FFF8BA4870 +:109CD000243808F0B5FE002D02D02068283020601E +:109CE00038BD30B5B54D04466878A04200D8FFDFD6 +:109CF000686800EB041030BD70B5B04800252C46F4 +:109D0000467807E02046FFF7ECFF4078641C2844C3 +:109D1000C5B2E4B2B442F5D1284630E72DE9F041AE +:109D20000C4607464FF0000800F01FF90646FF28D2 +:109D300001D94FF013083868C01C20F003023A60C4 +:109D400054EA080421D19D48F3B2072128300DF0D0 +:109D5000DBFD09E0072C10D2DFE804F00604080858 +:109D60000A040600974804E0974802E0974800E09C +:109D700097480DF0E9FD054600E0FFDFA54200D061 +:109D8000FFDF641CE4B2072CE4D3386800EB061054 +:109D9000386040467BE5021D5143452900D24521EC +:109DA0000844C01CB0FBF2F0C0B270472DE9FC5F64 +:109DB000064682484FF000088B464746444690F8D6 +:109DC000019022E02046FFF78CFF050000D1FFDF65 +:109DD000687869463844C7B22846FEF7A3FF824632 +:109DE00001A92846FEF7B8FF0346BDF80400524615 +:109DF000001D81B2BDF80000001D80B20AF0D4F849 +:109E00006A78641C00FB0288E4B24C45DAD1306801 +:109E1000C01C20F003003060BBF1000F00D0002018 +:109E2000424639460AF0CEF8316808443060BDE851 +:109E3000FC9F6249443108710020C87070475F4937 +:109E40004431CA782AB10A7801EB421108318142C3 +:109E500001D001207047002070472DE9F0410646EF +:109E60000078154600F00F0400201080601E0F4699 +:109E7000052800D3FFDF50482A46183800EB84003D +:109E8000394650F8043C3046BDE8F04118472DE90A +:109E9000F0414A4E0C46402806D0412823D04228A3 +:109EA0002BD0432806D123E0A07861780D18E17803 +:109EB000814201D90720EAE42078012801D9132042 +:109EC000E5E4FF2D08D80BF009FF07460DF046F931 +:109ED000381A801EA84201DA1220D8E42068B06047 +:109EE000207930730DE0BDE8F041084600F078B805 +:109EF00008780228DED8307703E008780228D9D81D +:109F000070770020C3E4F8B500242C4DA02805D0BC +:109F1000A12815D0A22806D00720F8BD087800F0A7 +:109F20000100E8771FE00E4669463046FDF73DFD2B +:109F30000028F2D130882884B07885F8220012E019 +:109F400008680921F82801D3820701D00846F8BD26 +:109F50006A7C02F00302012A04D16A8BD73293B2E1 +:109F60008342F3D868622046F8BD2DE9F047DFF858 +:109F70004C900026344699F8090099F80A2099F87F +:109F800001700244D5B299F80B20104400F0FF088C +:109F900008E02046FFF7A5FE817B407811FB0066B4 +:109FA000641CE4B2BC42F4D199F8091099F80A0093 +:109FB0002944294441440DE054610200B0030020CB +:109FC0001C0000206741000045B30000DD2F0000A9 +:109FD000FB56010000B1012008443044BDE8F08781 +:109FE00038B50446407800F00300012803D0022869 +:109FF0000BD0072038BD606858B1F7F777F9D0B9B2 +:10A000006068F7F76AF920B915E06068F7F721F999 +:10A0100088B969462046FCF729F80028EAD160781B +:10A0200000F00300022808D19DF8000028B1606804 +:10A03000F7F753F908B1102038BD6189F8290DD818 +:10A04000208988420AD8607800F003020A48012A71 +:10A0500006D1D731426A89B28A4201D2092038BD7D +:10A0600094E80E0000F1100585E80E000AB9002101 +:10A070000183002038BD0000B00300202DE9F0412D +:10A08000074614468846084601F08AFD064608EB56 +:10A0900088001C22796802EBC0000D18688C58B14A +:10A0A0004146384601F08BFD014678680078C200D1 +:10A0B000082305F120000CE0E88CA8B141463846A1 +:10A0C00001F084FD0146786808234078C20005F15C +:10A0D000240009F0A8FD38B1062121726681D0E97B +:10A0E0000010C4E9031009E0287809280BD00520E6 +:10A0F000207266816868E060002028702046BDE814 +:10A10000F04101F02EBD072020726681F4E72DE9B1 +:10A11000F04116460D460746406801EB85011C22BA +:10A1200002EBC1014418204601F072FD40B100214C +:10A13000708865F30F2160F31F4106200DF0BEFC0F +:10A1400009202070324629463846BDE8F04195E79F +:10A150002DE9F0410E46074600241C21F07816E058 +:10A1600004EB8403726801EBC303D25C6AB1FFF7AE +:10A170003DFA050000D1FFDF6F802A4621463046B8 +:10A18000FFF7C5FF0120BDE8F081641CE4B2A042E6 +:10A19000E6D80020F7E770B5064600241C21C078F9 +:10A1A0000AE000BF04EB8403726801EBC303D51817 +:10A1B0002A782AB1641CE4B2A042F3D8402070BDD2 +:10A1C00028220021284604F062F9706880892881DD +:10A1D000204670BD70B5034600201C25DC780CE0DD +:10A1E00000EB80065A6805EBC6063244167816B1B5 +:10A1F000128A8A4204D0401CC0B28442F0D8402067 +:10A2000070BDF0B5044600201C26E5780EE000BFC6 +:10A2100000EB8007636806EBC7073B441F788F425B +:10A2200002D15B78934204D0401CC0B28542EFD883 +:10A230004020F0BD0078032801D0002070470120A5 +:10A2400070470078022801D0002070470120704735 +:10A250000078072801D000207047012070472DE9C1 +:10A26000F041064688461078F1781546884200D3BA +:10A27000FFDF2C781C27641CF078E4B2A04201D8E0 +:10A28000201AC4B204EB8401706807EBC1010844D2 +:10A29000017821B14146884708B12C7073E72878CE +:10A2A000A042E8D1402028706DE770B514460B88B5 +:10A2B0000122A240134207D113430B8001230A223B +:10A2C000011D09F07AFC047070BD2DE9FF4F81B0CB +:10A2D0000878DDE90E7B9A4691460E4640072CD45D +:10A2E000019809F026FF040000D1FFDF07F1040800 +:10A2F00020461FFA88F109F065F8050000D1FFDF5C +:10A30000204629466A4609F0B0FA0098A0F8037082 +:10A31000A0F805A0284609F056FB017869F306016C +:10A320006BF3C711017020461FFA88F109F08DF810 +:10A3300000B9FFDF019807F094F906EB0900017FEF +:10A34000491C017705B0BDE8F08F2DE9F84F0E46A6 +:10A350009A4691460746032109F0A8FD0446008D60 +:10A36000DFF8B885002518B198F80000B0421ED17A +:10A37000384609F0DEFE070000D1FFDF09F10401D5 +:10A38000384689B209F01EF8050010D03846294633 +:10A390006A4609F06AFA009800210A460180817035 +:10A3A00007F01CFA0098C01DCAF8000021E098F8D8 +:10A3B0000000B04216D104F1260734F8341F012002 +:10A3C00000FA06F911EA090F00D0FFDF2088012307 +:10A3D00040EA090020800A22391D384609F008FCAD +:10A3E000067006E0324604F1340104F12600FFF75E +:10A3F0005CFF0A2188F800102846BDE8F88FFEB5FA +:10A4000015460C46064602AB0C220621FFF79DFFBF +:10A41000002827D00299607812220A70801C4870A8 +:10A4200008224A80A07002982988052381806988C3 +:10A43000C180A9880181E988418100250C20CDE9EE +:10A440000005062221463046FFF73FFF294600223D +:10A4500066F31F41F02310460DF086FA6078801CE9 +:10A4600060700120FEBDFEB514460D46062206466C +:10A4700002AB1146FFF769FF002812D0029B1320A0 +:10A4800000211870A8785870022058809C800620FF +:10A49000CDE900010246052329463046FFF715FFA6 +:10A4A0000120FEBD2DE9FE430C46804644E002AB90 +:10A4B0000E2207214046FFF748FF002841D0606880 +:10A4C0001C2267788678BF1C06EB860102EBC1016F +:10A4D000451802981421017047700A214180698A49 +:10A4E0000181E98A4181A9888180A98981813046D9 +:10A4F00001F056FB029905230722C8806F700420E3 +:10A50000287000250E20CDE9000521464046FFF7C2 +:10A51000DCFE294666F30F2168F31F41F023002279 +:10A5200006200DF021FA6078FD49801C6070626899 +:10A530002046921CFFF793FE606880784028B6D1D1 +:10A540000120BDE8FE83FEB50D46064638E002ABAD +:10A550000E2207213046FFF7F8FE002835D0686844 +:10A560001C23C17801EB810203EBC202841802981C +:10A5700015220270627842700A224280A2894281CA +:10A58000A2888281084601F00BFB01460298818077 +:10A59000618AC180E18A0181A088B8B10020207061 +:10A5A00000210E20CDE9000105230722294630466F +:10A5B000FFF78BFE6A68DB492846D21CFFF74FFE87 +:10A5C0006868C0784028C2D10120FEBD0620E6E7B9 +:10A5D0002DE9FE430C46814644E0204601F002FB93 +:10A5E000D0B302AB082207214846FFF7AEFE002891 +:10A5F000A7D060681C2265780679AD1C06EB860141 +:10A6000002EBC10147180298B7F8108006210170CB +:10A61000457004214180304601F0C2FA014602989B +:10A6200005230722C180A0F804807D7008203870BF +:10A630000025CDE9000521464846FFF746FE29469C +:10A6400066F30F2169F31F41F023002206200DF06D +:10A650008BF96078801C60706268B3492046121DD7 +:10A66000FFF7FDFD606801794029B6D1012068E758 +:10A670002DE9F34F83B00D4691E0284601F0B2FA80 +:10A6800000287DD068681C2290F806A00AEB8A0199 +:10A6900002EBC10144185146284601F097FAA1780F +:10A6A000CB0069684978CA00014604F1240009F02A +:10A6B000D6FA07468188E08B4FF00009091A8EB25E +:10A6C00008B1C84607E04FF00108504601F053FAC0 +:10A6D00008B9B61CB6B2208BB04200D80646B346C5 +:10A6E00002AB324607210398FFF72FFE060007D082 +:10A6F000B8F1000F0BD0504601F03DFA10B106E062 +:10A7000000201FE60299B8884FF0020908800196E0 +:10A71000E28B3968ABEB09001FFA80F80A44039812 +:10A720004E46009209F005FDDDE90021F61D434685 +:10A73000009609F014F9E08B404480B2E083B988B8 +:10A74000884201D1012600E00026CDE900B6238A27 +:10A75000072229460398FFF7B8FD504601F00BFA8F +:10A7600010B9E089401EE08156B1A078401CA0706D +:10A770006868E978427811FB02F1CAB2012300E06F +:10A7800007E081690E3009F018FA80F800A0002077 +:10A79000E0836A6865492846921DFFF760FD686896 +:10A7A000817940297FF469AF0120CBE570B5064679 +:10A7B00048680D4614468179402910D104EB840184 +:10A7C0001C2202EBC101084401F043FA002806D024 +:10A7D0006868294684713046BDE8704048E770BD1E +:10A7E000FEB50C460746002645E0204601F0FAF982 +:10A7F000D8B360681C22417901EB810102EBC101F1 +:10A800004518688900B9FFDF02AB082207213846E6 +:10A81000FFF79BFD002833D00299607816220A705A +:10A82000801C4870042048806068407901F0B8F9C5 +:10A83000014602980523072281806989C18008208A +:10A84000CDE9000621463846FFF73FFD6078801CC1 +:10A850006070A88969890844B0F5803F00D3FFDFA4 +:10A86000A88969890844A8816E81626830492046B8 +:10A87000521DFFF7F4FC606841794029B5D10120F1 +:10A88000FEBD30B5438C458BC3F3C704002345B1EF +:10A89000838B641EED1AC38A6D1E1D4495FBF3F372 +:10A8A000E4B22CB1008918B1A04200D8204603447C +:10A8B0004FF6FF70834200D3034613800C7030BD07 +:10A8C0002DE9FC41074616460D46486802EB860115 +:10A8D0001C2202EBC10144186A4601A92046FFF779 +:10A8E000D0FFA089618901448AB2BDF8001091426D +:10A8F00012D0081A00D5002060816868407940288D +:10A900000AD1204601F09BF9002805D06868294645 +:10A9100046713846FFF764FFBDE8FC813000002037 +:10A9200035A2000043A2000051A2000053BC000069 +:10A930003FBC00002DE9FE4F0F468146154650886A +:10A94000032109F0B3FA0190B9F8020001F01BF9F4 +:10A9500082460146019801F045F9002824D001986B +:10A960001C2241680AEB8A0002EBC0000C1820464A +:10A9700001F04EF9002817D1B9F80000E18A8842A9 +:10A980000ED8A18961B1B8420ED100265146019876 +:10A9900001F015F9218C01EB0008608B30B114E057 +:10A9A000504601F0E8F8A0B3BDE8FE8F504601F034 +:10A9B000E2F808B1678308E0022FF5D3B9F8040084 +:10A9C0006083618A884224D80226B81B87B2B8F80F +:10A9D0000400A28B801A002814DD874200DA384672 +:10A9E0001FFA80FB688869680291D8F800100A4451 +:10A9F000009209F08CFBF61D009A5B4602990096C6 +:10AA000008F079FFA08B384480B2A083618B884224 +:10AA100007D96888019903B05246BDE8F04F01F0AC +:10AA200035B91FD14FF009002872B9F802006881CA +:10AA3000D8E90010C5E90410608BA881284601F010 +:10AA400090F85146019801F0BAF8014601980823A0 +:10AA500040680078C20004F1200009F0E4F800200A +:10AA6000A0836083504601F086F810B9A089401E8B +:10AA7000A0816888019903B00AF0FF02BDE8F04F99 +:10AA80001EE72DE9F041064615460F461C461846BE +:10AA9000F6F7DFFB18B92068F6F701FC10B11020BB +:10AAA000BDE8F0817168688C0978B0EBC10F01D303 +:10AAB0001320F5E73946304601F081F80146706809 +:10AAC00008230078C20005F1200009F076F8D4E9E7 +:10AAD0000012C0E900120020E2E710B5044603218D +:10AAE00009F0E4F90146007800F00300022805D0DF +:10AAF0002046BDE8104001F1140280E48A8A204615 +:10AB0000BDE81040AFE470B50446032109F0CEF96A +:10AB1000054601462046FFF75BFD002816D0294672 +:10AB20002046FFF75DFE002810D029462046FFF79B +:10AB30000AFD00280AD029462046FFF7B3FC00286A +:10AB400004D029462046BDE8704091E570BD2DE94E +:10AB5000F0410C4680461EE0E178427811FB02F19C +:10AB6000CAB2816901230E3009F05DF80778606888 +:10AB70001C22C179491EC17107EB8701606802EB95 +:10AB8000C10146183946204601F02CF818B130466C +:10AB900001F037F820B16068C1790029DCD17FE786 +:10ABA000FEF724FD050000D1FFDF0A202872384699 +:10ABB00000F0F6FF68813946204601F007F80146AB +:10ABC000606808234078C20006F1240009F02BF8E1 +:10ABD000D0E90010C5E90310A5F80280284600F06E +:10ABE000C0FFB07800B9FFDFB078401EB07057E703 +:10ABF00070B50C460546032109F058F90146406836 +:10AC0000C2792244C2712846BDE870409FE72DE911 +:10AC1000FE4F8246507814460F464FF00008002839 +:10AC20004FD0012807D0022822D0FFDF2068B8606B +:10AC30006068F860B8E602AB0E2208215046FFF7C4 +:10AC400084FB0028F2D00298152105230170217899 +:10AC500041700A214180C0F80480C0F80880A0F843 +:10AC60000C80628882810E20CDE90008082221E054 +:10AC7000A678304600F094FF054606EB86012C22AC +:10AC8000786802EBC1010822465A02AB11465046D1 +:10AC9000FFF75BFB0028C9D00298072101702178DB +:10ACA00041700421418008218580C680CDE90018CB +:10ACB00005230A4639465046FFF707FB87F8088008 +:10ACC00072E6A678022516B1022E13D0FFDF2A1DE8 +:10ACD000914602AB08215046FFF737FB0028A5D06C +:10ACE00002980121022E01702178417045808680F2 +:10ACF00002D005E00625EAE7A188C180E18801814C +:10AD0000CDE900980523082239465046D4E710B50E +:10AD10000446032109F0CAF8014600F10802204662 +:10AD2000BDE8104073E72DE9F04F0F4605468DB0A2 +:10AD300014465088032109F0B9F84FF000088DF847 +:10AD400014800646ADF81680042F7BD36A78002A5B +:10AD500078D028784FF6FF794FF01C0A132834D0AA +:10AD600008DC012871D006284AD007286ED01228A6 +:10AD70000ED106E014286AD0152869D0162807D10C +:10AD8000AAE10C2F04D1307800F00301022907D08A +:10AD9000CDF80880CDF80C8068788DF808004CE07C +:10ADA00040F0080030706878B07001208DF8140011 +:10ADB000A888ADF81800E888ADF81A002889ADF821 +:10ADC0001C006889ADF81E0011E1B078904239D1BD +:10ADD0003078010736D5062F34D120F008003070C6 +:10ADE0006088414660F31F4100200CF067FE02209E +:10ADF0008DF81400ADF81890A888ADF81A00F6E0A8 +:10AE0000082F1FD1A888EF88814600F0BCFE80463D +:10AE10000146304600F0E6FE18B1404600F0ABFEB9 +:10AE2000B8B1FC48D0E90010CDE902106878ADF85F +:10AE30000C908DF80800ADF80E70608802AA3146BB +:10AE4000FFF7E5FE0DB0BDE8F08FB6E01EE041E093 +:10AE5000ECE0716808EB88002C2202EBC000085A75 +:10AE6000B842EFD1EB4802AAD0E90210CDE90210B6 +:10AE700068788DF8080008F0FF058DF80A506088A2 +:10AE80003146FFF7C4FE224629461FE0082FD9D1DC +:10AE9000B5F80480E88800F076FE074601463046A3 +:10AEA00000F0A0FE0028CDD007EB870271680AEB06 +:10AEB000C2000844028A4245C4D101780829C1D1A0 +:10AEC000407869788842BDD1F9B222463046FFF712 +:10AED0001EF9B7E70E2F7FF45BAFE9886F898B46C9 +:10AEE000B5F808903046FFF775F9ABF140014029FD +:10AEF00001D309204AE0B9F1170F01D3172F01D26E +:10AF00000B2043E040280ED000EB800271680AEB72 +:10AF1000C20008440178012903D140786978884249 +:10AF200090D00A2032E03046FFF735F9014640283C +:10AF30002BD001EB810372680AEBC30002EB00081F +:10AF4000012288F800206A7888F801207068AA88B1 +:10AF50004089B84200D93846AD8903232372A282C2 +:10AF6000E7812082A4F80C906582084600F018FE64 +:10AF70006081A8F81490A8F81870A8F80E50A8F8E6 +:10AF800010B0204600F0EDFD5CE7042005212172A1 +:10AF9000A4F80A80E081012121739E49D1E90421AE +:10AFA000CDE9022169788DF80810ADF80A006088B3 +:10AFB00002AA3146FFF72BFEE3E7062F89D3B078CC +:10AFC00090421AD13078010717D520F00800307070 +:10AFD0006088414660F31F4100200CF06FFD0220A5 +:10AFE0008DF81400A888ADF81800ADF81A906088A4 +:10AFF000224605A9F9F7E3F824E704213046FFF7D4 +:10B0000000F905464028BFD0022083030090224665 +:10B010002946304600F003FE4146608865F30F2163 +:10B0200060F31F4106200CF049FD0BE70E2FABD15A +:10B0300004213046FFF7E5F881464028A4D0414678 +:10B04000608869F30F2160F31F4106200CF036FD84 +:10B05000A8890B906889099070682F894089B84247 +:10B0600000D938468346B5F80680A8880A90484635 +:10B0700000F096FD60810B9818B1022000900B9BA8 +:10B0800024E0B8F1170F1ED3172F1CD30420207211 +:10B0900009986082E781A4F810B0A4F80C8009EB4D +:10B0A000890271680AEBC2000D18DDE90913A5F8E1 +:10B0B0001480A5F818B0E9812B82204600F051FDDC +:10B0C00006202870BEE601200B2300902246494648 +:10B0D000304600F0A4FDB5E6082F8DD1A988304692 +:10B0E000FFF778F80746402886D000F044FD002896 +:10B0F0009BD107EB870271680AEBC20008448046C7 +:10B1000000F086FD002890D1ED88B8F80E002844A4 +:10B11000B0F5803F05D360883A46314600F0B6FD71 +:10B1200090E6002DCED0A8F80E0060883A46314651 +:10B13000FFF73CFB08202072384600F031FD6081AB +:10B14000A5811EE72DE9F05F0C4601281FD09579F7 +:10B1500092F8048092F8056005EB85011F2202EB4E +:10B16000C10121F0030B08EB060111FB05F14FF6BD +:10B17000FF7202EAC10909F1030115FB0611264F0E +:10B1800021F0031ABB6840B101283ED125E0616877 +:10B19000E57891F800804E78DEE75946184608F0C9 +:10B1A000C0FC606000B9FFDF5A460021606803F010 +:10B1B0006EF9E5705146B86808F0B3FC6168486103 +:10B1C00000B9FFDF6068426902EB090181616068D4 +:10B1D00080F800806068467017E0606852464169F8 +:10B1E000184608F0C9FC5A466168B86808F0C4FC03 +:10B1F000032008F003FE0446032008F007FE201A8F +:10B20000012802D1B86808F081FC0BEB0A00BDE808 +:10B21000F09F000060610200300000200246002123 +:10B2200002208FE7F7B5FF4C0A20164620700098E1 +:10B2300060B100254FEA0D0008F055FC0021A17017 +:10B240006670002D01D10099A160FEBD012500208E +:10B25000F2E770B50C46154638220021204603F06F +:10B2600016F9012666700A22002104F11C0003F081 +:10B270000EF905B9FFDF297A207861F3010020700B +:10B28000A87900282DD02A4621460020FFF75AFF32 +:10B2900061684020E34A88706168C870616808711D +:10B2A000616848716168887161682888088161688F +:10B2B00068884881606886819078002811D061682C +:10B2C0000620087761682888C885616828884886CC +:10B2D00060680685606869889288018681864685EF +:10B2E000828570BDC878002802D00022012029E79D +:10B2F000704770B50546002165F31F4100200CF032 +:10B30000DDFB0321284608F0D1FD040000D1FFDF5A +:10B3100021462846FEF71CFF002804D0207840F084 +:10B3200010002070012070BD70B505460C4603204A +:10B3300008F056FD08B1002070BDBA4885708480C1 +:10B34000012070BD2DE9FF4180460E460F0CFEF72F +:10B350004DF9050007D06F800321384608F0A6FD9F +:10B36000040008D106E004B03846BDE8F0411321DE +:10B37000F9F750BBFFDF5FEA080005D0B8F1060F10 +:10B3800018D0FFDFBDE8FF8120782A4620F00800B2 +:10B3900020700020ADF8020002208DF800004FF66A +:10B3A000FF70ADF80400ADF8060069463846F8F7BE +:10B3B00006FFE7E7C6F3072101EB81021C23606863 +:10B3C00003EBC202805C042803D008280AD0FFDF08 +:10B3D000D8E7012000904FF440432A46204600F071 +:10B3E0001EFCCFE704B02A462046BDE8F041FEF738 +:10B3F0008EBE2DE9F05F05464089002790460C4639 +:10B400003E46824600F0BFFB8146287AC01E0828CF +:10B410006BD2DFE800F00D04192058363C47722744 +:10B420001026002C6CD0D5E90301C4E902015CE0D0 +:10B4300070271226002C63D00A2205F10C0104F1BA +:10B44000080002F0FAFF50E071270C26002C57D0BC +:10B45000E868A06049E0742710269CB3D5E9030191 +:10B46000C4E902016888032108F020FD8346FEF745 +:10B47000BDF802466888508049465846FEF7FEFDF2 +:10B4800033E075270A26ECB1A88920812DE07627C4 +:10B490001426BCB105F10C0004F1080307C883E8C9 +:10B4A000070022E07727102664B1D5E90301C4E93B +:10B4B00002016888032108F0F9FC01466888FFF75B +:10B4C00046FB12E01CE073270826CCB168880321F4 +:10B4D00008F0ECFC01460078C00606D56888FEF747 +:10B4E00037FE10B96888F8F777FAA8F800602CB131 +:10B4F0002780A4F806A066806888A080002086E6E1 +:10B50000A8F80060FAE72DE9FC410C461E461746F4 +:10B510008046032108F0CAFC05460A2C0AD2DFE85F +:10B5200004F005050505050509090907042303E0DD +:10B53000062301E0FFDF0023CDE9007622462946FD +:10B540004046FEF7C2FEBDE8FC81F8B50546A0F511 +:10B550007F40FF382BD0284608F0D9FD040000D1E9 +:10B56000FFDF204608F05FF9002821D001466A4637 +:10B57000204608F07AF900980321B0F805602846C3 +:10B5800008F094FC0446052E13D0304600F0FBFA78 +:10B5900005460146204600F025FB40B1606805EBFA +:10B5A00085013E2202EBC101405A002800D0012053 +:10B5B000F8BD007A0028FAD00020F8BDF8B504469E +:10B5C000408808F0A4FD050000D1FFDF6A46284648 +:10B5D000616800F0C4FA01460098091F8BB230F888 +:10B5E000032F0280428842800188994205D1042AB3 +:10B5F00008D0052A20D0062A16D022461946FFF781 +:10B6000099F9F8BD001D0E46054601462246304612 +:10B61000F6F739FF0828F4D1224629463046FCF7D0 +:10B6200064F9F8BD30000020636864880A46011D93 +:10B630002046FAF789F9F4E72246001DFFF773FB6D +:10B64000EFE770B50D460646032108F02FFC040015 +:10B6500004D02078000704D5112070BD43F2020009 +:10B6600070BD2A4621463046FEF7C9FE18B9286843 +:10B6700060616868A061207840F0080020700020B8 +:10B6800070BD70B50D460646032108F00FFC04009E +:10B6900004D02078000704D4082070BD43F20200D3 +:10B6A00070BD2A4621463046FEF7DDFE00B9A58270 +:10B6B000207820F008002070002070BD2DE9F04FA8 +:10B6C0000E4691B08046032108F0F0FB0446404648 +:10B6D00008F02FFD07460020079008900990ADF86C +:10B6E00030000A9002900390049004B9FFDF0DF13E +:10B6F0000809FFB9FFDF1DE038460BA9002207F05B +:10B7000055FF9DF82C0000F07F050A2D00D3FFDFC8 +:10B710006019017F491E01779DF82C00000609D5AC +:10B720002A460CA907A8FEF7C0FD19F80510491C08 +:10B7300009F80510761EF6B2DED204F13400F84D99 +:10B7400004F1260BDFF8DCA304F12A07069010E0D1 +:10B750005846069900F08CFA064628700A2800D34D +:10B76000FFDF5AF8261040468847E08CC05DB042A3 +:10B7700002D0208D0028EBD10A202870E94D4E46DA +:10B7800028350EE00CA907A800F072FA0446375DD0 +:10B7900055F8240000B9FFDF55F82420394640460B +:10B7A0009047BDF81E000028ECD111B0BDE8F08F25 +:10B7B00010B5032108F07AFB040000D1FFDF0A2254 +:10B7C000002104F11C0002F062FE207840F0040029 +:10B7D000207010BD10B50C46032108F067FB204413 +:10B7E000007F002800D0012010BD2DE9F84F8946C8 +:10B7F00015468246032108F059FB070004D028466D +:10B80000F5F727FD40B903E043F20200BDE8F88FE9 +:10B810004846F5F744FD08B11020F7E7786828B1ED +:10B8200069880089814201D90920EFE7B9F8000051 +:10B830001C2488B100F0A7F980460146384600F084 +:10B84000D1F988B108EB8800796804EBC000085C86 +:10B8500001280BD00820D9E73846FEF79CFC80462B +:10B86000402807D11320D1E70520CFE7FDF7BEFE22 +:10B8700006000BD008EB8800796804EBC0000C18B8 +:10B88000B9F8000020B1E88910B113E01120BDE73C +:10B890002888172802D36888172801D20720B5E71F +:10B8A000686838B12B1D224641463846FFF7E9F853 +:10B8B0000028ABD104F10C0269462046FEF7E1FFF7 +:10B8C000288860826888E082B9F8000030B10220E0 +:10B8D0002070E889A080E889A0B12BE003202070C7 +:10B8E000A889A08078688178402905D180F80280F5 +:10B8F00039465046FEF7D6FD404600F051F9A9F80A +:10B90000000021E07868218B4089884200D90846F0 +:10B910002083A6F802A004203072B9F800007081DC +:10B92000E0897082F181208B3082A08AB08130461C +:10B9300000F017F97868C178402905D180F80380B4 +:10B9400039465046FEF7FFFD00205FE770B50D4613 +:10B950000646032108F0AAFA04000ED0284600F09B +:10B9600012F905460146204600F03CF918B1284678 +:10B9700000F001F920B1052070BD43F2020070BD56 +:10B9800005EB85011C22606802EBC101084400F050 +:10B990003FF908B1082070BD2A462146304600F024 +:10B9A00075F9002070BD2DE9F0410C461746804620 +:10B9B000032108F07BFA0546204600F0E4F804462F +:10B9C00095B10146284600F00DF980B104EB8401E1 +:10B9D0001C22686802EBC1014618304600F018F9D5 +:10B9E00038B10820BDE8F08143F20200FAE70520F3 +:10B9F000F8E73B46324621462846FFF742F8002842 +:10BA0000F0D1E2B229464046FEF75AFF708C083862 +:10BA1000082803D242484078F7F776FA0020E1E799 +:10BA20002DE9F0410D4617468046032108F03EFA05 +:10BA30000446284600F0A7F8064624B13846F5F734 +:10BA400008FC38B902E043F20200CBE73868F5F7AA +:10BA500000FC08B11020C5E73146204600F0C2F8CE +:10BA600060B106EB86011C22606802EBC10145183B +:10BA7000284600F0CDF818B10820B3E70520B1E75B +:10BA8000B888A98A884201D90C20ABE76168E88CA4 +:10BA90004978B0EBC10F01D31320A3E7314620460C +:10BAA00000F094F80146606808234078C20005F170 +:10BAB000240008F082F8D7E90012C0E90012F2B2BF +:10BAC00021464046FEF772FE00208BE72DE9F04745 +:10BAD0000D461F4690468146032108F0E7F90446CB +:10BAE000284600F050F806463CB14DB13846F5F70F +:10BAF000F4FB50B11020BDE8F08743F20200FAE7F2 +:10BB0000606858B1A0F80C8027E03146204600F06C +:10BB100069F818B1304600F02EF828B10520EAE7A0 +:10BB2000300000207861020006EB86011C2260686C +:10BB300002EBC1014518284600F06AF808B1082058 +:10BB4000D9E7A5F80880F2B221464846FEF7B8FECC +:10BB50001FB1A8896989084438800020CBE707F025 +:10BB600084BE017821F00F01491C21F0F001103151 +:10BB70000170FDF73EBD20B94E48807808B1012024 +:10BB80007047002070474B498988884201D10020C6 +:10BB90007047402801D2402000E0403880B2704712 +:10BBA00010B50446402800D9FFDF2046FFF7E3FF29 +:10BBB00010B14048808810BD4034A0B210BD40682C +:10BBC00042690078484302EBC0007047C278406881 +:10BBD000037812FB03F24378406901FB032100EB79 +:10BBE000C1007047C2788A4209D9406801EB8101DF +:10BBF0001C2202EBC101405C08B10120704700200B +:10BC000070470078062801D901207047002070474E +:10BC10000078062801D00120704700207047F0B45A +:10BC200001EB81061C27446807EBC6063444049DDB +:10BC300005262670E3802571F0BCFEF71FBA10B50B +:10BC4000418911B1FFF7DDFF08B1002010BD0120CF +:10BC500010BD10B5C18C8278B1EBC20F04D9C18977 +:10BC600011B1FFF7CEFF08B1002010BD012010BDBB +:10BC700010B50C4601230A22011D07F0D4FF0078FD +:10BC80002188012282409143218010BDF0B402EB53 +:10BC900082051C264C6806EBC505072363554B68D7 +:10BCA0001C79402C03D11A71F0BCFEF791BCF0BC9A +:10BCB000704700003000002010B5EFF3108000F056 +:10BCC000010472B6FC484178491C41704078012853 +:10BCD00001D10BF0B3FA002C00D162B610BD70B5E3 +:10BCE000F54CA07848B90125A570FFF7E5FF0BF0EA +:10BCF000B6FA20B100200BF080FA002070BD4FF0A2 +:10BD00008040E570C0F80453F7E770B5EFF310809A +:10BD100000F0010572B6E84C607800B9FFDF60788A +:10BD2000401E6070607808B90BF08CFA002D00D1CD +:10BD300062B670BDE04810B5817821B10021C170B4 +:10BD40008170FFF7E2FF002010BD10B504460BF034 +:10BD500086FAD9498978084000D001202060002067 +:10BD600010BD10B5FFF7A8FF0BF079FA02220123EE +:10BD7000D149540728B1D1480260236103200872D9 +:10BD800002E00A72C4F804330020887110BD2DE966 +:10BD9000F84FDFF824934278817889F80420002650 +:10BDA00089F80510074689F806600078DFF810B3B7 +:10BDB000354620B1012811D0022811D0FFDF0BF049 +:10BDC00060FA4FF0804498B10BF062FAB0420FD1A4 +:10BDD00030460BF061FA0028FAD042E00126EEE787 +:10BDE000FFF76AFF58460168C907FCD00226E6E75C +:10BDF0000120E060C4F80451B2490E600107D1F897 +:10BE00004412B04AC1F3423124321160AD49343199 +:10BE100008604FF0020AC4F804A3A060AA480168B1 +:10BE2000C94341F3001101F10108016841F010011B +:10BE3000016001E019F0A8FFD4F804010028F9D04E +:10BE400030460BF029FA0028FAD0B8F1000F04D1DF +:10BE50009D48016821F010010160C4F808A3C4F8EE +:10BE6000045199F805004E4680B1387870B90BF04E +:10BE7000F6F980460BF006FC6FF00042B8F1000FB7 +:10BE800002D0C6E9032001E0C6E90302DBF80000A6 +:10BE9000C00701D00BF0DFF9387810B13572BDE87A +:10BEA000F88F4FF01808C4F808830127A7614FF4F2 +:10BEB0002070ADF8000000BFBDF80000411EADF8D5 +:10BEC0000010F9D2C4F80C51C4F810517A48C01DC2 +:10BED0000BF06CFA3570FFF744FF676179493079F0 +:10BEE00020310860C4F80483D9E770B5050000D19B +:10BEF000FFDF4FF080424FF0FF30C2F8080300210F +:10BF0000C2F80011C2F80411C2F80C11C2F81011E5 +:10BF1000694C61700BF0AFF910B10120A070607036 +:10BF200067480068C00701D00BF095F92846BDE8C6 +:10BF300070402CE76048007A002800D0012070474C +:10BF40002DE9F04F61484FF0000A85B0D0F800B0FD +:10BF5000D14657465D4A5E49083211608406D4F8DE +:10BF6000080110B14FF0010801E04FF000080BF09C +:10BF7000F0F978B1D4F8240100B101208246D4F858 +:10BF80001C0100B101208146D4F8200108B101272D +:10BF900000E00027D4F8000100B101200490D4F89B +:10BFA000040100B101200390D4F80C0100B101207C +:10BFB0000290D4F8100100B101203F4D0190287883 +:10BFC00000260090B8F1000F04D0C4F808610120E9 +:10BFD0000BF013F9BAF1000F04D0C4F82461092062 +:10BFE0000BF00BF9B9F1000F04D0C4F81C610A2062 +:10BFF0000BF003F927B1C4F820610B200BF0FDF81A +:10C000002D48C01D0BF0DAF900B1FFDFDFF8AC807E +:10C010000498012780B1C4F80873E87818B1EE706D +:10C0200000200BF0EAF8287A022805D103202872B4 +:10C030000221C8F800102761039808B1C4F8046110 +:10C04000029850B1C4F80C61287A032800D0FFDFB1 +:10C05000C8F800602F72FFF758FE019838B1C4F895 +:10C060001061287A012801D100F05CF8676100981E +:10C0700038B12E70287A012801D1FFF772FEFFF740 +:10C0800044FE0D48C01D0BF0AFF91049091DC1F861 +:10C0900000B005B0BDE8F08F074810B5C01D0BF02B +:10C0A0008DF90549B0B1012008704FF0E021C1F8C9 +:10C0B0000002BDE81040FFE544000020340C0040C1 +:10C0C0000C0400401805004010ED00E0100502408F +:10C0D00001000001087A012801D1FFF742FEBDE806 +:10C0E000104024480BF080B970B5224CE41FA078B2 +:10C0F00008B90BF0A7F801208507A861207A00266F +:10C10000032809D1D5F80C0120B900200BF0C4F8A0 +:10C110000028F7D1C5F80C6126724FF0FF30C5F842 +:10C12000080370BD70B5134CE41F6079F0B10128AD +:10C1300003D0A179401E814218DA0BF090F8054631 +:10C140000BF0A0FA6179012902D9A179491CA171EA +:10C150000DB1216900E0E168411A022902DA11F10A +:10C16000020F06DC0DB1206100E0E060BDE8704028 +:10C17000F7E570BD4B0000200F4A12680D498A4256 +:10C180000CD118470C4A12680A4B9A4206D101B5E5 +:10C190000BF04AFA0BF01DFDBDE8014007490968A4 +:10C1A0000958084706480749054A064B70470000EA +:10C1B00000000000BEBAFECA5C000020040000209F +:10C1C000C8130020C8130020F8B51D46DDE9064756 +:10C1D0000E000AD007F0ADFF2346FF1DBCB231466A +:10C1E0002A46009407F0BBFBF8BDD0192246194639 +:10C1F00002F023F92046F8BD70B50D460446102222 +:10C20000002102F044F9258117206081A07B40F0D5 +:10C210000A00A07370BD4FF6FF720A80014602202B +:10C220000BF04CBC704700897047827BD30701D16B +:10C23000920703D48089088000207047052070474A +:10C24000827B920700D581817047014600200988D2 +:10C2500041F6FE52114200D00120704700B503465E +:10C26000807BC00701D0052000BD59811846FFF72B +:10C27000ECFFC00703D0987B40F004009873987BD4 +:10C2800040F001009873002000BD827B520700D56A +:10C2900009B14089704717207047827B61F3C30260 +:10C2A000827370472DE9FC5F0E460446017896467E +:10C2B000012000FA01F14DF6FF5201EA020962681D +:10C2C0004FF6FF7B1188594502D10920BDE8FC9F3C +:10C2D000B9F1000F05D041F6FE55294201D00120E9 +:10C2E000F4E741EA090111801D0014D000232B70EE +:10C2F00094F800C0052103221F464FF0020ABCF14A +:10C300000E0F76D2DFE80CF0F909252F47646B7722 +:10C31000479193B4D1D80420D8E7616820898B7BFA +:10C320009B0767D517284AD30B89834247D389894E +:10C33000172901D3814242D185F800A0A5F8010058 +:10C340003280616888816068817B21F0020181739D +:10C35000C6E0042028702089A5F801006089A5F8AE +:10C3600003003180BCE0208A3188C01D1FFA80F8AC +:10C37000414524D3062028702089A5F80100608952 +:10C38000A5F80300A089A5F805000721208ACDE9BA +:10C390000001636941E00CF0FF00082810D008207C +:10C3A00028702089A5F801006089A5F80300318074 +:10C3B0006A1D694604F10C0009F025FB10B15EE02E +:10C3C0001020EDE730889DF800100844308087E0A9 +:10C3D0000A2028702089A5F80100328044E00C2052 +:10C3E00028702089A5F801006089A5F80300318034 +:10C3F0003AE082E064E02189338800EB41021FFAD1 +:10C4000082F843453BD3B8F1050F38D30E222A708A +:10C410000BEA4101CDE90010E36860882A467146C5 +:10C42000FFF7D2FEA6F800805AE04020287060890D +:10C430003188C01C1FFA80F8414520D32878714606 +:10C4400020F03F00123028702089A5F80100608993 +:10C45000CDE9000260882A46E368FFF7B5FEA6F83A +:10C460000080287840063BD461682089888037E0C6 +:10C47000A0893288401D1FFA80F8424501D2042766 +:10C480003DE0162028702089A5F801006089A5F8F4 +:10C490000300A089CDE9000160882A46714623691E +:10C4A000FFF792FEA6F80080DEE718202870207AB9 +:10C4B0006870A6F800A013E061680A88920401D4AD +:10C4C00005271CE0C9882289914201D0062716E081 +:10C4D0001E21297030806068018821F4005101809C +:10C4E000B9F1000F0BD061887823002202200BF0F5 +:10C4F0003BFA61682078887006E033800327606823 +:10C50000018821EA090101803846DFE62DE9FF4F65 +:10C5100085B01746129C0D001E461CD03078C1070E +:10C5200003D000F03F00192801D9012100E00021CB +:10C530002046FFF7AAFEA8420DD32088A0F57F4130 +:10C54000FF3908D03078410601D4000605D508200F +:10C5500009B0BDE8F08F0720FAE700208DF8000051 +:10C560008DF8010030786B1E00F03F0C0121A81EF1 +:10C570004FF0050A4FF002094FF0030B9AB2BCF1DD +:10C58000200F75D2DFE80CF08B10745E7468748C29 +:10C59000749C74B574BA74C874D474E1747474F10E +:10C5A00074EF74EE74ED748B052D78D18DF80090D6 +:10C5B000A0788DF804007088ADF8060030798DF809 +:10C5C0000100707800F03F000C2829D00ADCA0F1AF +:10C5D0000200092863D2DFE800F0126215621A62D5 +:10C5E0001D622000122824D004DC0E281BD0102845 +:10C5F000DBD11BE016281FD01828D6D11FE02078E9 +:10C60000800701E020784007002848DAEEE0207833 +:10C610000007F9E72078C006F6E720788006F3E700 +:10C6200020784006F0E720780006EDE72088C00576 +:10C63000EAE720884005E7E720880005E4E720884E +:10C64000C004E1E72078800729D5032D27D18DF894 +:10C6500000B0B6F8010081E0217849071FD5062D0A +:10C660001DD381B27078012803D0022817D102E0CF +:10C67000C9E0022000E0102004228DF8002072782A +:10C680008DF80420801CB1FBF0F2ADF8062092B2C8 +:10C6900042438A4203D10397ADF80890A6E079E0BF +:10C6A0002078000776D598B282088DF800A0ADF802 +:10C6B0000420B0EB820F6DD10297ADF8061095E023 +:10C6C0002178C90666D5022D64D381B206208DF883 +:10C6D0000000707802285DD3B1FBF0F28DF8040001 +:10C6E000ADF8062092B242438A4253D1ADF8089089 +:10C6F0007BE0207880064DD5072003E020784006B7 +:10C700007FD508208DF80000A088ADF80400ADF8B2 +:10C710000620ADF8081068E02078000671D50920E1 +:10C72000ADF804208DF80000ADF8061002975DE02A +:10C730002188C90565D5022D63D381B20A208DF801 +:10C740000000707804285CD3C6E72088400558D5DF +:10C75000012D56D10B208DF80000A088ADF8040003 +:10C7600044E021E026E016E0FFE72088000548D5F8 +:10C77000052D46D30C208DF80000A088ADF80400EC +:10C78000B6F803006D1FADF80850ADF80600ADF81F +:10C790000AA02AE035E02088C00432D5012D30D12E +:10C7A0000D208DF8000021E02088800429D4B6F8FF +:10C7B0000100E080A07B000723D5032D21D3307832 +:10C7C00000F03F001B2818D00F208DF800002088B3 +:10C7D00040F40050A4F80000B6F80100ADF80400E1 +:10C7E000ED1EADF80650ADF808B003976946059800 +:10C7F000F5F792FB050008D016E00E208DF800003A +:10C80000EAE7072510E008250EE0307800F03F0049 +:10C810001B2809D01D2807D0022005990BF04EF9DE +:10C82000208800F400502080A07B400708D52046D7 +:10C83000FFF70BFDC00703D1A07B20F00400A0731D +:10C84000284685E61FB5022806D101208DF8000094 +:10C8500088B26946F5F760FB1FBD0000F8B51D46BC +:10C86000DDE906470E000AD007F063FC2346FF1DF2 +:10C87000BCB231462A46009407F071F8F8BDD019D1 +:10C880002246194601F0D9FD2046F8BD2DE9FF4F9B +:10C890008DB09B46DDE91B57DDF87CA00C46082BCC +:10C8A00005D0E06901F0FEF850B11020D2E02888F0 +:10C8B000092140F0100028808AF80010022617E0B5 +:10C8C000E16901208871E2694FF420519180E169AA +:10C8D0008872E06942F601010181E06900218173FB +:10C8E0002888112140F0200028808AF800100426B2 +:10C8F00038780A900A2038704FF0020904F11800C5 +:10C900004D460C9001F0C6FBB04681E0BBF1100F24 +:10C910000ED1022D0CD0A9EB0800801C80B20221A0 +:10C92000CDE9001005AB52461E990D98FFF796FF12 +:10C93000BDF816101A98814203D9F74800790F9074 +:10C9400004E003D10A9808B138702FE04FF00201DB +:10C95000CDE900190DF1160352461E990D98FFF707 +:10C960007DFF1D980088401B801B83B2C6F1FF002D +:10C97000984200D203461E990BA8D9B15FF000027D +:10C98000DDF878C0CDE9032009EB060189B2CDE9D5 +:10C9900001C10F980090BDF8161000220D9801F00B +:10C9A0000EFC387070B1C0B2832807D0BDF81600F5 +:10C9B00020833AE00AEB09018A19E1E7022011B06D +:10C9C000BDE8F08FBDF82C00811901F0FF08022DA1 +:10C9D0000DD09AF80120424506D1BDF820108142C1 +:10C9E00007D0B8F1FF0F04D09AF801801FE08AF851 +:10C9F0000180C94800680178052902D1BDF81610E8 +:10CA0000818009EB08001FFA80F905EB080085B268 +:10CA1000DDE90C1005AB0F9A01F03FFB28B91D981A +:10CA20000088411B4145BFF671AF022D13D0BBF109 +:10CA3000100F0CD1A9EB0800801C81B20220CDE9B7 +:10CA4000000105AB52461E990D98FFF707FF1D9890 +:10CA50000580002038700020B1E72DE9F8439C469E +:10CA6000089E13460027B26B9AB3491F8CB2F18F10 +:10CA7000A1F57F45FF3D05D05518AD882944891D96 +:10CA80008DB200E000252919B6F83C8008314145F7 +:10CA900020D82A44BCF8011022F8021BBCF803106D +:10CAA00022F8021B984622F8024B914607F02FFB12 +:10CAB0004FF00C0C41464A462346CDF800C006F024 +:10CAC0001AFFF587B16B00202944A41D214408807A +:10CAD00003E001E0092700E083273846BDE8F8833A +:10CAE00010B50B88848F9C420CD9846BE0180488A5 +:10CAF00044B1848824F40044A41D23440B801060B6 +:10CB0000002010BD0A2010BD2DE9F0478AB0002595 +:10CB1000904689468246ADF8185007274BE00598A5 +:10CB200006888088000446D4A8F8006007A801950C +:10CB300000970295CDE903504FF40073002231466F +:10CB4000504601F03CFB04003CD1BDF81800ADF8A4 +:10CB50002000059804888188B44216D10A0414D4B0 +:10CB600001950295039521F400410097049541F445 +:10CB7000804342882146504601F0BFF804000BD1A3 +:10CB80000598818841F40041818005AA08A948469A +:10CB9000FFF7A6FF0400DCD00097059802950195E9 +:10CBA000039504950188BDF81C300022504601F021 +:10CBB000A4F80A2C06D105AA06A94846FFF790FF5B +:10CBC0000400ACD0ADF8185004E00598818821F439 +:10CBD0000041818005AA06A94846FFF781FF002889 +:10CBE000F3D00A2C03D020460AB0BDE8F08700201D +:10CBF000FAE710B50C46896B86B051B10C218DF85F +:10CC00000010A18FADF80810A16B01916946FAF7E9 +:10CC100001FB00204FF6FF71A063E187A08706B0FB +:10CC200010BD2DE9F0410D460746896B0020069E98 +:10CC30001446002911D0012B0FD13246294638461F +:10CC4000FFF762FF002808D1002C06D032462946A3 +:10CC50003846BDE8F04100F034BFBDE8F0812DE971 +:10CC6000FC411446DDE9087C0E46DDE90A15521D3B +:10CC7000BCF800E092B2964502D20720BDE8FC81E4 +:10CC8000ACF8002017222A70A5F80160A5F803303F +:10CC90000522CDE900423B462A46FFF7DFFD002092 +:10CCA000ECE770B50C46154648220021204601F0FD +:10CCB000EEFB04F1080044F81C0F00204FF6FF7152 +:10CCC000E06161842084A5841720E08494F82A0020 +:10CCD00040F00A0084F82A0070BD4FF6FF720A8007 +:10CCE000014603200AF0EABE30B585B00C46054681 +:10CCF000FFF77FFFA18E284629B101218DF8001092 +:10CD00006946FAF787FA0020E0622063606305B0A5 +:10CD100030BDB0F8400070476000002090F8462019 +:10CD2000920703D4408808800020F4E70620F2E749 +:10CD300090F846209207EED5A0F84410EBE70146A4 +:10CD4000002009880A0700D5012011F0F00F01D05A +:10CD500040F00200CA0501D540F004008A0501D563 +:10CD600040F008004A0501D540F010000905D2D571 +:10CD700040F02000CFE700B5034690F84600C0071A +:10CD800001D0062000BDA3F842101846FFF7D7FFD8 +:10CD900010F03E0F05D093F8460040F0040083F8F1 +:10CDA000460013F8460F40F001001870002000BD47 +:10CDB00090F84620520700D511B1B0F84200AAE71A +:10CDC0001720A8E710F8462F61F3C3020270A2E70C +:10CDD0002DE9FF4F9BB00E00DDE92B34DDE929780A +:10CDE000289D24D02878C10703D000F03F001928DF +:10CDF00001D9012100E000212046FFF7D9FFB04210 +:10CE000015D32878410600F03F010CD41E290CD020 +:10CE1000218811F47F6F0AD13A8842B1A1F57F428F +:10CE2000FF3A04D001E0122901D1000602D5042006 +:10CE30001FB0C5E5FA491D984FF0000A08718DF83A +:10CE400018A08DF83CA00FAA0A60ADF81CA0ADF8A0 +:10CE500050A02978994601F03F02701F5B1C04F135 +:10CE6000180C4FF0060E4FF0040BCDF858C01F2AD7 +:10CE70007ED2DFE802F07D7D107D267DAC7DF47DE5 +:10CE8000F37DF27DF17DF47DF07D7D7DEF7DEE7DA6 +:10CE90007D7D7D7DED0094F84610B5F80100890791 +:10CEA00001D5032E02D08DF818B01EE34FF40061B7 +:10CEB000ADF85010608003218DF83C10ADF84000B3 +:10CEC000D4E2052EEFD1B5F801002083ADF81C00A7 +:10CED000B5F80310618308B1884201D9012079E1D6 +:10CEE0000020A07220814FF6FF702084169801F078 +:10CEF000D1F8052089F800000220029083460AAB91 +:10CF00001D9A16991B9801F0C8F890BB9DF82E0049 +:10CF1000012804D0022089F80100102003E001203C +:10CF200089F8010002200590002203A90BA808F04F +:10CF30006AFDE8BB9DF80C00059981423DD1398816 +:10CF4000801CA1EB0B01814237DB02990220CDE965 +:10CF500000010DF12A034A4641461B98FFF77EFC6B +:10CF600002980BF1020B801C81B217AA029101E01A +:10CF70009CE228E003A90BA808F045FD02999DF862 +:10CF80000C00CDE9000117AB4A4641461B98FFF75C +:10CF900065FC9DF80C000AAB0BEB00011FFA81FB4E +:10CFA00002991D9A084480B2029016991B9800E0DD +:10CFB00003E001F072F80028B6D0BBF1020F02D0F6 +:10CFC000A7F800B04FE20A208DF818004BE20021CC +:10CFD0000391072EFFF467AFB5F801002083ADF889 +:10CFE0001C00B5F80320628300283FF477AF90421D +:10CFF0003FF674AF0120A072B5F805002081002033 +:10D00000A073E06900F04EFD78B9E16901208871F4 +:10D01000E2694FF420519180E1698872E16942F63A +:10D0200001000881E06900218173F01F20841E98AF +:10D03000606207206084169801F02CF8072089F8B8 +:10D0400000000120049002900020ADF82A0028E0A2 +:10D0500019E29FE135E1E5E012E2A8E080E043E07B +:10D060000298012814D0E0698079012803D1BDF825 +:10D070002800ADF80E00049803ABCDE900B04A4695 +:10D0800041461B98FFF7EAFB0498001D80B204900C +:10D09000BDF82A00ADF80C00ADF80E00059880B27E +:10D0A00002900AAB1D9A16991B9800F0F6FF28B95A +:10D0B00002983988001D05908142D1D2029801283A +:10D0C00081D0E0698079012803D1BDF82800ADF84E +:10D0D0000E00049803ABCDE900B04A4641461B98C8 +:10D0E000FFF7BCFB0298BDE1072E02D0152E7FF49E +:10D0F000DAAEB5F801102183ADF81C10B5F80320A5 +:10D10000628300293FF4EAAE91423FF6E7AE012187 +:10D11000A1724FF0000BA4F808B084F80EB0052EF1 +:10D1200007D0C0B2691DE26908F06BFC00287FF4EB +:10D130004AAF4FF6FF70208401A906AA14A8CDF8C3 +:10D1400000B081E885032878214600F03F031D9A4E +:10D150001B98FFF79BFB8246208BADF81C0082E1F9 +:10D160000120032EC3D14021ADF85010B5F80110B5 +:10D170002183ADF81C100AAAB8F1000F00D00023DB +:10D18000CDE9020304921D98CDF804800090388800 +:10D190000022401E83B21B9801F011F88DF8180090 +:10D1A00090BB0B2089F80000BDF8280035E04FF057 +:10D1B000010C052E9BD18020ADF85000B5F8011070 +:10D1C0002183B5F803002084ADF81C10B0F5007F72 +:10D1D00003D907208DF8180087E140F47C422284AF +:10D1E0000CA8B8F1000F00D00023CDE90330CDE941 +:10D1F000018C1D9800903888401E83B21B9800F067 +:10D20000DEFF8DF8180018B18328A8D10220BFE0F6 +:10D210000D2189F80010BDF83000401C22E100000B +:10D2200060000020032E04D248067FF53CAE0020AB +:10D2300018E1B5F80110ADF81C102878400602D5A9 +:10D240008DF83CE002E007208DF83C004FF000082C +:10D250000320CDE902081E9BCDF810801D98019394 +:10D26000A6F1030B00901FFA8BF342461B9800F0C7 +:10D2700044FD8DF818008DF83C80297849060DD5BD +:10D280002088C00506D5208BBDF81C10884201D12E +:10D29000C4F8248040468DF81880E3E0832801D14B +:10D2A0004FF0020A4FF48070ADF85000BDF81C003A +:10D2B0002083A4F820B01E986062032060841321AC +:10D2C000CDE0052EFFF4EFADB5F80110ADF81C1060 +:10D2D000A28F6AB3A2F57F43FE3B29D008228DF8C6 +:10D2E0003C2000BF4FF0000B0523CDE9023BDDF8E9 +:10D2F00078C0CDF810B01D9A80B2CDF804C040F4CB +:10D3000000430092B5F803201B9800F0F6FC8DF85E +:10D310003CB04FF400718DF81800ADF85010832820 +:10D3200010D0F8B1A18FA1F57F40FE3807D0DCE026 +:10D330000B228DF83C204FF6FE72A287D2E7A4F8AC +:10D340003CB0D2E000942B4631461E9A1B98FFF762 +:10D3500084FB8DF8180008B183284BD1BDF81C0060 +:10D36000208353E700942B4631461E9A1B98FFF703 +:10D3700074FB8DF81800E8BBE18FA06B0844831D97 +:10D380008DE888034388828801881B98FFF767FC33 +:10D39000824668E095F80180022E70D15FEA0800AD +:10D3A00002D0B8F1010F6AD109208DF83C0007A81E +:10D3B00000908DF840804346002221461B98FFF7DD +:10D3C00030FC8DF842004FF0000B8DF843B050B99F +:10D3D000B8F1010F12D0B8F1000F04D1A18FA1F55F +:10D3E0007F40FF380AD0A08F40B18DF83CB04FF499 +:10D3F000806000E037E0ADF850000DE00FA91B9809 +:10D40000F9F708FF82468DF83CB04FF48060ADF824 +:10D410005000BAF1020F06D0FC480068C07928B16C +:10D420008DF8180027E0A4F8188044E0BAF1000F46 +:10D4300003D081208DF818003DE007A800904346F6 +:10D44000012221461B98FFF7ECFB8DF818002146BE +:10D450001B98FFF7CEFB9DF8180020B9192189F819 +:10D460000010012038809DF83C0020B10FA91B98C6 +:10D47000F9F7D0FE8246BAF1000F33D01BE018E076 +:10D480008DF818E031E02078000712D5012E10D178 +:10D490000A208DF83C00E088ADF8400003201B997D +:10D4A0000AF00CFB0820ADF85000C0E648067FF5F6 +:10D4B000FAAC4FF0040A2088BDF8501008432080D1 +:10D4C000BDF8500080050BD5A18FA1F57F40FE3837 +:10D4D00006D11E98E06228982063A6864FF0030AC2 +:10D4E0005046A5E49DF8180078B1012089F80000A5 +:10D4F000297889F80110BDF81C10A9F802109DF8D0 +:10D50000181089F80410052038802088BDF85010C4 +:10D5100088432080E4E72DE9FF4F8846087895B0DE +:10D52000012181404FF20900249C0140ADF82010F8 +:10D530002088DDF88890A0F57F424FF0000AFF3A7E +:10D5400006D039B1000705D5012019B0BDE8F08F2C +:10D550000820FAE7239E4FF0000B0EA886F800B0D3 +:10D5600018995D460988ADF83410A8498DF81CB0AB +:10D57000179A0A718DF838B0086098F800000128F1 +:10D580003BD0022809D003286FD1307820F03F002B +:10D590001D303070B8F80400E08098F800100320C7 +:10D5A000022904D1317821F03F011B31317094F808 +:10D5B0004610090759D505ABB9F1000F13D000216A +:10D5C00002AA82E80B000720CDE90009BDF834006B +:10D5D000B8F80410C01E83B20022159800F0EFFDC9 +:10D5E0000028D1D101E0F11CEAE7B8F80400A6F860 +:10D5F0000100BDF81400C01C04E198F805108DF876 +:10D600001C1098F80400012806D04FF4007A022874 +:10D610002CD00328B8D16CE12188B8F8080011F4A7 +:10D620000061ADF8201020D017281CD3B4F84010AA +:10D63000814218D3B4F84410172901D3814212D182 +:10D64000317821F03F01C91C3170A6F80100032197 +:10D65000ADF83410A4F8440094F8460020F002001D +:10D6600084F8460065E105257EE177E1208808F130 +:10D67000080700F4FE60ADF8200010F0F00F1BD09A +:10D6800010F0C00F03D03888228B9042EBD199B9AB +:10D69000B878C00710D0B9680720CDE902B1CDF83D +:10D6A00004B00090CDF810B0FB88BA88398815987E +:10D6B00000F023FB0028D6D12398BDF82010401C91 +:10D6C00080294ED006DC10290DD020290BD040290E +:10D6D00087D124E0B1F5807F6ED051457ED0B1F581 +:10D6E000806F97D1DEE0C80601D5082000E0102049 +:10D6F00082460DA907AA0520CDE902218DF8380040 +:10D70000ADF83CB0CDE9049608A93888CDE9000110 +:10D710005346072221461598FFF7B8F8A8E09DF870 +:10D720001C2001214FF00A0A002A9BD105ABB9F158 +:10D73000000F00D00020CDE902100720CDE900093C +:10D74000BDF834000493401E83B2218B002215984B +:10D7500000F035FD8DF81C000B203070BDF8140072 +:10D7600020E09DF81C2001214FF00C0A002A22D154 +:10D7700013ABB9F1000F00D00020CDE90210072053 +:10D78000CDE900090493BDF83400228C401E83B219 +:10D79000218B159800F013FD8DF81C000D203070C2 +:10D7A000BDF84C00401CADF8340005208DF8380061 +:10D7B000208BADF83C00BCE03888218B88427FF498 +:10D7C00052AF9DF81C004FF0120A00281CD1606A6D +:10D7D000A8B1B878C0073FF446AF00E018E0BA68D7 +:10D7E0000720CDE902B2CDF804B00090CDF810B01A +:10D7F000FB88BA88159800F080FA8DF81C00132079 +:10D8000030700120ADF8340093E00000600000208B +:10D810003988208B8142D2D19DF81C004FF0160A26 +:10D820000028A06B08D0E0B34FF6FF7000215F46E0 +:10D83000ADF808B0019027E068B1B978C907BED14A +:10D84000E18F0DAB0844821D03968DE80C024388DE +:10D850008288018809E0B878C007BCD0BA680DABEF +:10D8600003968DE80C02BB88FA881598FFF7F7F944 +:10D8700005005ED0072D72D076E0019005AA02A9BE +:10D880002046FFF72DF90146E28FBDF808008242DD +:10D8900001D00029F1D0E08FA16B084407800198E6 +:10D8A000E08746E09DF81C004FF0180A40B1208B3D +:10D8B000C8B13888208321461598FFF79AF938E0D7 +:10D8C00004F118000090237E012221461598FFF7ED +:10D8D000A8F98DF81C000028EDD119203070012026 +:10D8E000ADF83400E7E7052521461598FFF781F9E3 +:10D8F0003AE0208800F40070ADF8200050452DD1AA +:10D90000A08FA0F57F41FE3901D006252CE0D8F884 +:10D9100008004FF0160A48B1A063B8F80C10A187B0 +:10D920004FF6FF71E187A0F800B002E04FF6FF70FC +:10D93000A087BDF8200030F47F611AD07823002240 +:10D94000032015990AF010F898F80000207120883B +:10D95000BDF82010084320800EE000E00725208855 +:10D96000BDF8201088432080208810F47F6F1CD0E1 +:10D970003AE02188814321809DF8380020B10EA92A +:10D980001598F9F747FC05469DF81C000028EBD0D8 +:10D9900086F801A001203070208B70809DF81C005B +:10D9A00030710520ADF83400DEE7A18EE1B11898A2 +:10D9B0000DAB0088ADF834002398CDE90304CDE920 +:10D9C0000139206B0090E36A179A1598FFF700FA67 +:10D9D000054601208DF838000EA91598F9F71AFCB4 +:10D9E00000B10546A4F834B094F8460040070AD5C3 +:10D9F0002046FFF7A4F910F03E0F04D114F8460FAB +:10DA000020F0040020701898BDF8341001802846DA +:10DA10009BE500B585B0032806D102208DF80000F3 +:10DA200088B26946F9F7F6FB05B000BD10B5384C71 +:10DA30000B782268012B02D0022B2AD111E0137837 +:10DA40000BB1052B01D10423137023688A889A80B7 +:10DA50002268CB88D38022680B8913814989518140 +:10DA60000DE08B8893802268CB88D38022680B8955 +:10DA700013814B8953818B899381096911612168D5 +:10DA8000F9F7C8FB226800210228117003D0002892 +:10DA900000D0812010BD832010BD806B002800D0F5 +:10DAA000012070478178012909D10088B0F5205FF5 +:10DAB00003D042F60101884201D1002070470720BF +:10DAC0007047F0B587B0002415460E460746ADF8FE +:10DAD000184011E005980088288005980194811D60 +:10DAE000CDE902410721049400918388428801888E +:10DAF000384600F002F930B905AA06A93046FEF70B +:10DB0000EFFF0028E6D00A2800D1002007B0F0BDC2 +:10DB10006000002010B58B7883B102789A4205D15D +:10DB20000B885BB102E08B79091D4BB18B789A426F +:10DB3000F9D1B0F801300C88A342F4D1002010BD17 +:10DB4000812010BD072826D012B1012A27D103E079 +:10DB5000497801F0070102E04978C1F3C2010529C3 +:10DB60001DD2DFE801F00318080C12000AB10320EF +:10DB700070470220704704280DD250B10DE00528EF +:10DB800009D2801E022808D303E0062803D0032808 +:10DB900003D005207047002070470F207047812078 +:10DBA0007047C0B282060BD4000607D5FA48807AC7 +:10DBB0004143C01D01EBD00080B27047084670475A +:10DBC0000020704770B513880B800B781C0625D594 +:10DBD000F14CA47A844204D843F01000087000206D +:10DBE00070BD956800F0070605EBD0052D78F5406F +:10DBF00065F304130B701378D17803F0030341EA43 +:10DC0000032140F20123B1FBF3F503FB15119268E8 +:10DC1000E41D00FB012000EBD40070BD906870BDD6 +:10DC200037B51446BDF804101180117841F0040195 +:10DC300011709DF804100A061ED5D74AA368C1F3D7 +:10DC40000011927A824208D8FE2811D1D21DD20842 +:10DC50004942184600F01BFC0AE003EBD00200F03A +:10DC60000703012510789D40A84399400843107090 +:10DC7000207820F0100020703EBD2DE9F0410746CD +:10DC8000C81C0E4620F00300B04202D08620BDE83A +:10DC9000F081C14D002034462E60AF802881AA72E9 +:10DCA000E8801AE0E988491CE980810614D4E1780B +:10DCB00000F0030041EA002040F20121B0FBF1F244 +:10DCC00001FB12012068FFF76CFF2989084480B22C +:10DCD0002881381A3044A0600C3420784107E1D400 +:10DCE0000020D4E7AC4801220189C08800EB400045 +:10DCF00002EB8000084480B270472DE9FF4F89B0E5 +:10DD00001646DDE9168A0F46994623F44045084633 +:10DD100000F054FB040002D02078400703D4012017 +:10DD20000DB0BDE8F08F099806F086F802902078D3 +:10DD3000000606D59848817A0298814201D887204A +:10DD4000EEE7224601A90298FFF73CFF8346002038 +:10DD50008DF80C004046B8F1070F1AD00122214679 +:10DD6000FFF7F0FE0028DBD12078400611D5022015 +:10DD70008DF80C00ADF81070BDF80400ADF812007D +:10DD8000ADF814601898ADF81650CDF81CA0ADF899 +:10DD900018005FEA094004D500252E46A846012751 +:10DDA0000CE02178E07801F0030140EA012040F224 +:10DDB0000121B0FBF1F2804601FB12875FEA494086 +:10DDC00009D5B84507D1A178207901F0030140EACF +:10DDD0000120B04201D3BE4201D90720A0E7A81913 +:10DDE0001FFA80F9B94501D90D2099E79DF80C007B +:10DDF00028B103A90998F9F70BFA002890D1B84582 +:10DE000007D1A0784FEA192161F30100A07084F8CE +:10DE100004901A9800B10580199850EA0A0027D09A +:10DE2000199830B10BEB06002A46199900F005FB52 +:10DE30000EE00BEB06085746189E099806F067F9A6 +:10DE40002B46F61DB5B239464246009505F053FD06 +:10DE5000224601A90298FFF7B5FE9DF8040022466C +:10DE600020F010008DF80400DDE90110FFF7D8FE66 +:10DE7000002055E72DE9FF4FDFF81C91824685B061 +:10DE8000B9F80610D9F8000001EB410100EB81045C +:10DE900040F20120B2FBF0F1174600FB1175DDE9FD +:10DEA000138B4E4629460698FFF77BFE0346FFF785 +:10DEB00019FF1844B1880C30884202D9842009B077 +:10DEC0002FE70698C6B2300603D5B00601D5062066 +:10DED000F5E7B9F80620521C92B2A9F80620BBF16A +:10DEE000000F01D0ABF80020B00602D5C4F80880BE +:10DEF0000AE0B9F808201A4492B2A9F80820D9F823 +:10DF00000000891A0844A0602246FE200699FFF707 +:10DF100087FEE77025712078390A61F301002A0A2B +:10DF2000A17840F0040062F30101A17020709AF81A +:10DF300002006071BAF80000E08000252573300609 +:10DF400002D599F80A7000E00127B00601D54FF01C +:10DF500000084E4600244FF007090FE0CDE90258B3 +:10DF60000195CDF800900495F1882046129B089AFF +:10DF7000FFF7C3FE0028A2D1641CE4B2BC42EDD37B +:10DF800000209CE700B5FFF7ADFE03490C308A88FE +:10DF9000904203D9842000BD00060020CA8808688A +:10DFA00002EB420300EB8300521C037823F00403CE +:10DFB0000370CA80002101730846ECE72DE9F047A1 +:10DFC000804600F0FBF9070005D000264446F74DD7 +:10DFD00040F2012916E00120BDE8F087204600F05C +:10DFE000EDF90278C17802F0030241EA0222B2FBA5 +:10DFF000F9F309FB13210068FFF7D3FD3044641CDB +:10E0000086B2A4B2E988601E8142E7DCA8F1010073 +:10E01000E8802889801B288100203870DCE710B553 +:10E02000144631B1491E218005F006FFA070002082 +:10E0300010BD012010BD70B50446DC48C1880368DE +:10E0400001E0401C20802088884207D200EB40027B +:10E0500013EB820202D015786D07F2D580B28842A8 +:10E0600016D2AAB15079A072D08820819178107907 +:10E0700001F0030140EA0120A081A078E11CFFF734 +:10E08000A1FD20612088401C2080E080002070BD20 +:10E090000A2070BD0121018270472DE9FF4F85B034 +:10E0A0004FF6FF798246A3F8009048681E460D4659 +:10E0B00080788DF8060048680088ADF804000020DC +:10E0C0008DF80A00088A0C88A04200D304462C82EE +:10E0D00051E03878400708D4641C288AA4B2401C58 +:10E0E000288208F10100C0B246E0288A401C28823C +:10E0F000781D6968FFF70EFDD8BB3188494501D10D +:10E10000601E30803188A1EB080030806888A04212 +:10E1100038D3B878397900F0030041EA002801A922 +:10E12000781DFFF7F7FC20BB298949452ED0002236 +:10E1300039460798FFF706FDD8B92989414518D116 +:10E14000E9680391B5F80AC0D7F808B05046CDF891 +:10E1500000C005F0DCFFDDF800C05A460CF1070CEA +:10E160001FFA8CFC43460399CDF800C005F08DFBE7 +:10E1700060B1641CA4B200208046204600F01EF965 +:10E180000700A6D1641E2C820A2098E67480787954 +:10E19000B071F888B0803978F87801F0030140EA6E +:10E1A00001207081A6F80C80504605F045FE3A46E5 +:10E1B00006F10801FFF706FD306100207FE62DE93A +:10E1C000FF4F87B081461C469246DDF860B0DDF80F +:10E1D0005480089800F0F2F8050002D02878400733 +:10E1E00002D401200BB09CE5484605F025FE2978B5 +:10E1F000090605D56D49897A814201D88720F1E762 +:10E20000CAF309062A4601A9FFF7DCFC0746149861 +:10E2100007281CD000222946FFF794FC0028E1D1F2 +:10E220002878400613D501208DF808000898ADF82D +:10E230000C00BDF80400ADF80E00ADF81060ADF8AC +:10E24000124002A94846F8F7E3FF0028CAD129780E +:10E25000E87801F0030140EA0121AA78287902F068 +:10E26000030240EA0220564507D0B1F5007F04D9E9 +:10E27000611E814201DD0B20B4E7864201D90720EF +:10E28000B0E7801B85B2A54200D92546BBF1000F3F +:10E2900001D0ABF80050179818B1B9192A4600F010 +:10E2A000CCF8B8F1000F0DD03E4448464446169FC6 +:10E2B00005F03FFF2146FF1DBCB232462B460094BD +:10E2C00005F04DFB00208DE72DE9F04107461D4686 +:10E2D0001646084600F072F8040002D02078400785 +:10E2E00001D40120D3E4384605F0A6FD21780906C3 +:10E2F00005D52E49897A814201D88720C7E4224674 +:10E300003146FFF75FFC65B12178E07801F0030149 +:10E3100040EA0120B0F5007F01D8012000E0002094 +:10E3200028700020B3E42DE9F04107461D4616464B +:10E33000084600F043F8040002D02078400701D4DA +:10E340000120A4E4384605F077FD2178090605D5BB +:10E350001649897A814201D8872098E422463146BD +:10E36000FFF75EFCFF2D14D02178E07801F0030266 +:10E3700040EA022040F20122B0FBF2F302FB13005C +:10E3800015B900F2012080B2E070000A60F30101CB +:10E39000217000207BE410B50C4600F00FF810B19E +:10E3A0000178490704D4012010BD000000060020B8 +:10E3B000C18821804079A0700020F5E70749CA880C +:10E3C000824209D340B1096800EB40006FF00B02B4 +:10E3D00002EB8000084470470020704700060020D0 +:10E3E00070B504460D4621462B460AB9002070BD83 +:10E3F00001E0491C5B1C501E021E03D008781E78E9 +:10E40000B042F6D008781E78801BF0E730B50C4695 +:10E4100001462346051B954206D202E0521E9D5C32 +:10E420008D54002AFAD107E004E01D780D70491CD4 +:10E430005B1C521E002AF8D130BDF0B50E460146D5 +:10E44000334680EA030404F00304B4B906E002B9D9 +:10E45000F0BD13F8017B01F8017B521E01F00307A8 +:10E46000002FF4D10C461D4602E080CD80C4121F5F +:10E47000042AFAD221462B4600BF04E013F8014BD0 +:10E4800001F8014B521E002AF8D100BFE0E7F0B5B9 +:10E490000C460146E6B204E002B9F0BD01F8016B9A +:10E4A000521E01F00307002FF6D10B46E5B245EAF4 +:10E4B000052545EA054501E020C3121F042AFBD2C9 +:10E4C000194602E001F8016B521E002AFAD100BF82 +:10E4D000E3E7000010B509F0A0FDF4F7F9F909F041 +:10E4E000E7FBBDE8104009F0AFBC302834BF012085 +:10E4F00000207047202834BF4FF0A0420C4A01236F +:10E5000000F01F0003FA00F0002914BFC2F80C0548 +:10E51000C2F808057047202834BF4FF0A0410449D5 +:10E5200000F01F00012202FA00F0C1F81805704740 +:10E530000003005070B50346002002466FF02F051F +:10E540000EE09C5CA4F130060A2E02D34FF0FF309F +:10E5500070BD00EB800005EB4000521C2044D2B29D +:10E560008A42EED370BD30B50A230BE0B0FBF3F462 +:10E5700003FB1404B0FBF3F08D183034521E05F881 +:10E58000014CD2B2002AF1D130BD30B500234FF694 +:10E59000FF7510E0040A44EA002084B2C85C6040C1 +:10E5A000C0F30314604005EA00344440E0B25B1C51 +:10E5B00084EA40109BB29342ECD330BD2DE9F04188 +:10E5C000FE4B0026012793F864501C7893F868C02E +:10E5D000B8B183F89140A3F8921083F8902083F8A3 +:10E5E0008E70BCF1000F0CBF83F8946083F89450D8 +:10E5F000F3488068008805F08AFDBDE8F04105F029 +:10E6000021BA4FF6FF7083F89140A3F8920083F887 +:10E61000902083F88E70BCF1000F14BF83F89450E3 +:10E6200083F89460BDE8F0812DE9F041E44D29685C +:10E6300091F89C200024012A23D091F89620012AE9 +:10E6400030D091F86C301422DC4E0127012B32D0EF +:10E6500091F88E30012B4FD091F8A620012A1CBFD3 +:10E660000020BDE8F08144701F2200F8042B222214 +:10E67000A731FFF7E2FE286880F8A6400120BDE838 +:10E68000F08144701B220270D1F89D204260D1F8C5 +:10E69000A120826091F8A520027381F89C4001209E +:10E6A000BDE8F081447007220270D1F898204260E2 +:10E6B00081F89640E2E78046447000F8042B20225F +:10E6C0006E31FFF7BAFE88F80870286880F86C4051 +:10E6D00090F86E000028D1D1B6F87000A6F8980026 +:10E6E000A868417B86F89A1086F89670008805F035 +:10E6F0000EFD05F0B6F9C1E791F86C30012B0BD097 +:10E70000447017220270D1F890204260B1F8942032 +:10E71000028181F88E40B1E78046447000F8042BF6 +:10E7200020226E31FFF789FE88F80870286880F88B +:10E730006C4090F86E000028A0D1CDE7A04800689A +:10E7400090F86C10002914BFB0F870004FF6FF70FD +:10E75000704770B59A4C06462068002808BFFFDF56 +:10E760000025206845706660002808BFFFDF20682C +:10E77000417800291CBFFFDF70BDCC220021FFF7CC +:10E7800086FE2068FF2101707F2180F83810132158 +:10E790004184282180F86910012180F85C1080F8FC +:10E7A00061500AF0C1F9BDE8704009F0AEBA844981 +:10E7B0000968097881420CBF012000207047804819 +:10E7C000006890F82200C0F3400070477C48006861 +:10E7D00090F8220000F0010070477948006890F836 +:10E7E0002200C0F3001070472DE9F0437448002464 +:10E7F000036893F82400B3F822C0C0F38001C0F38B +:10E800004002114400F001000844CCF3001121B390 +:10E81000BCF1100F02BF6B4931F81000BDE8F08366 +:10E82000BCF1120F18BFBCF1130F0ED0BCF1150FC5 +:10E830001EBFFFDF2046BDE8F0830021624A32F8A8 +:10E84000102010FB0120BDE8F083604A002132F85F +:10E85000102010FB0120BDE8F08393F85E2093F8B0 +:10E860005F102E264FF47A774FF014084FF04009CE +:10E87000022A04BF4AF2D745B5FBF7F510D0012AAA +:10E8800004BF4AF22F75B5FBF7F510D04AF62315F1 +:10E89000B5FBF7F5082A08BF4E4613D0042A18D056 +:10E8A0002646082A0ED0042A13D0022A49D004F1A1 +:10E8B0002806042A0FD0082A1CBF4FF01908082286 +:10E8C00004D00AE04FF0140806F5A8764FF0400295 +:10E8D00003E006F5A8764FF0100218FB026212FB67 +:10E8E0000052C0EB00103A4D00EB800005EB8000B9 +:10E8F00010441CF0010F4FF4C8724FF4BF7504BFF1 +:10E90000CCF34006002E65D0CCF3400600F5A57090 +:10E91000EEB1082904BF174640260CD0042904BFD5 +:10E920002F46102607D0022907BF04F11807042636 +:10E9300004F12807082606EB860808EB86163E44F5 +:10E940001BE004F118064FF019080422C5E7082956 +:10E9500004BF164640270CD0042904BF2E461027BA +:10E9600007D0022907BF04F11806042704F128067E +:10E97000082707EB871706EB8706304400F19C0653 +:10E9800093F8690001F00C07002F08BF0020304405 +:10E9900018BF00F5416027D1082904BF164640275B +:10E9A0001BD0042904BF2E46102716D0022906BF0B +:10E9B00004F11806042704F128060CE00C060020D8 +:10E9C00068000020DC610200E4610200D461020002 +:10E9D000D4FEFFFF64E018BF0827C7EBC70707EBAB +:10E9E000470706EB4706304498301CF0010F17D05C +:10E9F000082908BF40210CD0042904BF2A46102151 +:10EA000007D0022907BF04F11802042104F12802EB +:10EA1000082101EB410303EB0111114408443BE0E1 +:10EA2000082904BF944640260CD0042904BFAC46F4 +:10EA3000102607D0022907BF04F1180C042604F1A0 +:10EA4000280C082606EB8616B3F840300CEB860C33 +:10EA50006044EB2B20D944F2552C0B3303FB0CF311 +:10EA60009B0D082907D0042902D0022905D008E00F +:10EA70002A46102108E0402106E004F11802042192 +:10EA800002E004F12802082101EB811102EB81016F +:10EA900001F5A57103FB010000F5B470BDE8F0833A +:10EAA00000F5A570082904BF944640260CD004291F +:10EAB00004BFAC46102607D0022907BF04F1180C8A +:10EAC000042604F1280C082606EB8616B3F8483015 +:10EAD0000CEB860C6044EB2BDED944F2552C0B3347 +:10EAE00003FB0CF39B0D0829C5D00429C0D00229D3 +:10EAF000C7D1C2E7FE4840F271210068806A4843EE +:10EB00007047FB48006890F83700002818BF0120C4 +:10EB1000704710B5F74C207B022818BF032808D196 +:10EB2000207D04F115010EF0E6FE08281CBF01202F +:10EB300010BD207B002816BF022800200120BDE860 +:10EB400010400AF021BDEB4908737047E849096895 +:10EB500081F8300070472DE9F047E54C2168087BCB +:10EB6000002816BF022800200120487301F10E0181 +:10EB70000AF0F4FC2168087B022816BF0328012252 +:10EB8000002281F82F204FF0080081F82D00487BEB +:10EB900001F10E034FF001064FF00007012804BFFA +:10EBA0005B7913F0C00F0AD001F10E03012804D1E4 +:10EBB000587900F0C000402801D0002000E001207A +:10EBC00081F82E00002A04BF91F8220010F0040FF3 +:10EBD00007D0087D01F115010EF08DFE216881F846 +:10EBE0002D002068476007F0BFFA2168C14D4FF043 +:10EBF0000009886095F82D000EF089FE804695F892 +:10EC00002F00002818BFB8F1000F04D095F82D0090 +:10EC10000EF0B1FC68B195F8300000281CBF95F8E3 +:10EC20002E0000281DD0697B05F10E0001290ED0B1 +:10EC300012E06E734A4605F10E0140460AF0E4FC0C +:10EC400095F82D1005F10E000EF063FF09E04079F4 +:10EC500000F0C000402831D0394605F10E000AF01E +:10EC60000BFD2068C77690F8220010F0040F08BF53 +:10EC7000BDE8F087002795F82D000EF017FD050080 +:10EC800008BFBDE8F087102102F0C2F8002818BFC5 +:10EC9000BDE8F08720683A4600F11C01C676284698 +:10ECA0000AF0B2FC206800F11C0160680FF08EF8D9 +:10ECB0006068BDE8F04701210FF0A3B80EF066FFD1 +:10ECC0004A4605F10E010AF09FFCCAE7884A12681D +:10ECD000137B0370D2F80E000860508A888070475A +:10ECE00078B584490446824E407B087332682078A8 +:10ECF00010706088ADF8000080B200F00101C0F330 +:10ED0000400341EA4301C0F3800341EA8301C0F3B9 +:10ED1000C00341EAC301C0F3001341EA0311C0F389 +:10ED2000401341EA4311C0F3801041EA801050843F +:10ED3000E07D012808BF012507D0022808BF022571 +:10ED400003D0032814BFFFDF0825306880F85E5029 +:10ED5000607E012808BF012507D0022808BF0225D0 +:10ED600003D0032814BFFFDF0825316881F85F5006 +:10ED700091F83500012829D0207B81F82400488CA7 +:10ED80001D280CBF002060688862607D81F8370014 +:10ED9000A07B002816BF0228002001200875D4F8A7 +:10EDA0000F00C1F81500B4F81300A1F81900A07EF7 +:10EDB00091F86B2060F3071281F86B20E07E012848 +:10EDC00018BF002081F83400002078BD91F85E2043 +:10EDD0000420082A08BF81F85E00082D08BF81F8CA +:10EDE0005F00C9E742480068408CC0F3001131B1B0 +:10EDF000C0F38000002804BF1F20704702E0C0F36A +:10EE0000400109B10020704710F0010F14BFEE203F +:10EE1000FF20704736480068408CC0F3001119B1DC +:10EE2000C0F3800028B102E0C0F3400008B1002028 +:10EE30007047012070472E49002209684A664B8CB2 +:10EE40001D2B0CBF81F8682081F8680070470023F3 +:10EE5000274A126882F85D30D164A2F85000012080 +:10EE600082F85D007047224A0023126882F85C3005 +:10EE7000A2F858000120516582F85C0070471C49D7 +:10EE8000096881F8360070471949096881F86100FE +:10EE900070471748006890F961007047144800688F +:10EEA00090F82200C0F3401070471148006890F8B5 +:10EEB0002200C0F3C0007047012070470C48006872 +:10EEC00090F85F00704770B509F018FE09F0F7FD83 +:10EED00009F0C0FC09F06CFD054C2068416E491C2E +:10EEE000416690F83300002558B109F01DFE03E09B +:10EEF000680000200C06002008F007FF206880F85A +:10EF000033502068457090F8391021B1BDE8704049 +:10EF100004200AF0AEBF90F86810D9B1406E81426B +:10EF200018D804200AF0A5FF206890F8220010F0FD +:10EF3000010F07D0A06843220188BDE8704001207E +:10EF4000FFF73CBBBDE8704043224FF6FF71002045 +:10EF5000FFF734BBBDE8704000200AF08ABF2DE9FE +:10EF6000F04782B00F468146FE4E4FF000083068F1 +:10EF7000458C15F0030F10D015F0010F05F00200BD +:10EF800005D0002808BF4FF0010806D004E0002893 +:10EF900018BF4FF0020800D1FFDF4FF0000A5446BF +:10EFA00015F0010F05F002000DD080B915F0040F27 +:10EFB0000DD04AF00800002F1CBF40F0010040F0C7 +:10EFC00002044DD09EE010B115F0040F0DD015F0E5 +:10EFD000070F10D015F0010F05F0020043D00028F4 +:10EFE00008BF15F0040F34D04AE0002F18BF4AF0D4 +:10EFF000090444D141E037B14AF00800044615F055 +:10F00000200F1BD07EE0316805F02002B1F84800E7 +:10F01000104308BF4AF0010474D04AF018000446B7 +:10F0200015F0200F6ED191F85E1011F00C0118BF91 +:10F030000121C94361F30000044663E0316891F89F +:10F040005E1011F00C0118BF012161F300000446AD +:10F0500058E04AF00800002F18BF40F0010451D1D9 +:10F0600040F010044EE0002818BF15F0040F07D040 +:10F07000002F18BF4AF00B0444D14AF0180441E0B5 +:10F0800015F0030F3DD115F0040F3AD077B1306879 +:10F090004AF0080490F85E0010F00C0118BF01213E +:10F0A00061F3410415F0200F24D02BE0306805F007 +:10F0B0002002B0F84810114308BF4AF0030421D0E1 +:10F0C0004AF0180415F0200F0AD000BF90F85E0037 +:10F0D00010F00C0018BF0120C04360F3410411E0A0 +:10F0E00090F85E1011F00C0118BF0121C94361F3C3 +:10F0F0000004EBE710F00C0018BF012060F30004DF +:10F1000000E0FFDF15F0400F1CD0CFB93168B1F837 +:10F110004800002804BF488C10F0010F0BD110F0FC +:10F12000020F08BF10F0200F05D115F0010F08BF26 +:10F1300015F0020F04D091F85E0010F00C0F01D111 +:10F1400044F040047068A0F800A0017821F020018C +:10F1500001704FF007010EF005FE414670680EF099 +:10F16000F8FF214670680FF000F814F0010F0CD082 +:10F170004FF006034FF000027B4970680EF0CFFF9E +:10F180003068417B70680EF02FFE14F0020F18D02B +:10F19000D6E90010B9F1000F4FF006034FF001025D +:10F1A00007D01C310EF0BBFF012170680EF029FE64 +:10F1B00007E015310EF0B3FF3068017D70680EF086 +:10F1C00020FE14F0040F18BFFFDF14F0080F19D051 +:10F1D000CDF800A03068BDF800200223B0F86A1016 +:10F1E00061F30B02ADF8002090F86B0003220109D7 +:10F1F0009DF8010061F307108DF801006946706801 +:10F200000EF08DFF012F62D13068B0F84810E1B3E5 +:10F2100090F82200C0F34000B8BB70680EF095FF74 +:10F22000401CC7B23068C7F1FF05B0F84820B0F8FD +:10F230005A10511AA942B8BF0D46AA423BD990F8BC +:10F24000220010F0010F36D144F0100421467068FE +:10F250000EF08BFFF81CC0B2ED1E284482B230685D +:10F26000B0F86A10436EC1F30B0151FA83F190F8C4 +:10F2700060303E4F1944BC460023E1FB07C31B0925 +:10F280006FF0240C03FB0C1100E020E080F860100C +:10F2900090F85F00012101F01FF90090BDF8000017 +:10F2A0009DF80210032340EA01400190042201A9C5 +:10F2B00070680EF034FF3068AAB2416C70680EF0CE +:10F2C00082FF3068B0F85A102944A0F85A1014F0A0 +:10F2D000400F06D0D6E900100123062261310EF05E +:10F2E0001EFF14F0200F18BFFFDF0020002818BFFA +:10F2F000FFDF02B0BDE8F0872DE9F043194C89B07B +:10F300002068002808BFFFDF20684178002944D129 +:10F310000178FF2941D0002680F83160A0F85A60BA +:10F32000867080F83960304609F062FB104802AD03 +:10F3300000F1240191E80E1085E80E10D0E90D10BF +:10F34000CDE9061002A809F041FB08F0BCFF2068D7 +:10F3500090F9610009F090F8064809F093F8064822 +:10F360000CE00000680000201A06002053E4B36E91 +:10F37000C8610200D0610200CD61020009F012FBF9 +:10F38000606809F038FB206890F8240010F0010F45 +:10F3900007D0252009F07EF80AE009B00C20BDE86E +:10F3A000F08310F0020F18BF262069D009F072F820 +:10F3B000206890F85E10252008F043FF206880F850 +:10F3C0002C6009F00FFB206890F85E10002009F017 +:10F3D00028F90F21052008F0F8FF206890F82E107A +:10F3E000002901BF90F82F10002990F8220010F09A +:10F3F000040F74D006F0B8FE0546206829468068E0 +:10F4000007F0AAFBDFF82884074690FBF8F008FB1A +:10F4100010704142284606F08EFB2168886097FBF9 +:10F42000F8F04A68104448600EF062FA014620681D +:10F43000426891426ED8C0E90165FE4D4FF0010867 +:10F4400095F82D000EF063FA814695F82F000127FC +:10F45000002818BFB9F1000F04D095F82D000EF068 +:10F460008AF8A0B195F8300000281CBF95F82E004E +:10F47000002824D0687B05F10E01012815D019E081 +:10F4800010F0040F14BF2720FFDF8FD190E73A461A +:10F490006F7305F10E0148460AF0B6F895F82D1085 +:10F4A00005F10E000EF035FB09E0487900F0C000D0 +:10F4B000402815D0414605F10E000AF0DDF820681D +:10F4C00090F8220010F0040F24D095F82D000EF0D3 +:10F4D000EDF805001ED0102101F09AFC40B119E0B2 +:10F4E0000EF054FB3A4605F10E010AF08DF8E6E7FE +:10F4F00020683A4600F11C01C77628460AF084F8D5 +:10F50000206800F11C0160680EF060FC0121606859 +:10F510000EF077FC2068417B0E3008F038FF206841 +:10F5200090F85C1061B3B0F85810A0F84810416D25 +:10F53000416490F82210C1F30011F1B9B0F86A00EB +:10F540000221C0F30B05ADF80050684607F0B0FF8C +:10F5500028B1BDF80000C0F30B00A84204D1BDF8EB +:10F560000000401CADF800002168BDF80000B1F8B3 +:10F570006A2060F30B02A1F86A20206880F85C60C2 +:10F58000206890F85D1039B1B0F85010A0F8401024 +:10F59000C16CC16380F85D60B0F86A10426EC1F35F +:10F5A0000B0151FA82F190F86020DFF88CC211440F +:10F5B00063460022E1FB0C3212096FF0240302FBC8 +:10F5C000031180F860100EF00CFA032160680EF051 +:10F5D00090FA216881F8330009B00020BDE8F0837B +:10F5E0009649886070472DE9F043944C83B02268B7 +:10F5F00092F831303BB1508C1D2808BFFFDF03B0BB +:10F60000BDE8F0435FE401260027F1B1054692F81A +:10F61000600008F03FFF206890F85F10FF2008F0BE +:10F6200010FE20684FF4A57190F85F20002009F0CB +:10F63000D4F8206890F8221011F0030F00F02C810C +:10F64000002D00F0238100F027B992F822108046A7 +:10F65000D07EC1F30011002956D0054660680780AE +:10F66000017821F020010170518C132937D01FDC63 +:10F67000102908BF022144D0122908BF062140D01A +:10F68000FFDF6C4D606805F10E010EF091FB697BA8 +:10F6900060680EF0A9FB2068418C1D2918BF152950 +:10F6A00063D0B0F84820416C60680EF0B6FB5CE0B7 +:10F6B000152918BF1D29E3D14FF001010EF052FBAF +:10F6C0006068017841F020010170216885B11C312A +:10F6D0000EF07CFB012160680EF093FBD1E7002166 +:10F6E0000EF040FB6068017841F020010170C8E72E +:10F6F00015310EF06BFB2068017D60680EF081FB18 +:10F70000BFE70EF02FFBBCE70021FFF728FC606885 +:10F71000C17811F03F0F28D0017911F0100F24D0DB +:10F720000EF01EFB2368024693F82410C1F38000FC +:10F73000C1F3400C604401F00101084493F82C101F +:10F74000C1F3800CC1F34005AC4401F001016144F8 +:10F75000401AC1B293F85E0000F0BEFE0090032391 +:10F760000422694660680EF0DAFC2068002590F8F3 +:10F77000241090F82C0021EA000212F0010F18BFAB +:10F7800001250ED111F0020F04D010F0020F08BFB6 +:10F79000022506D011F0040F03D010F0040F08BFAB +:10F7A0000425B8F1000F2BD0012D1BD0022D08BF6E +:10F7B00026201BD0042D14BFFFDF272016D0206881 +:10F7C00090F85E10252008F03CFD206890F822108B +:10F7D000C1F3001169B101224FF49671002008F0C5 +:10F7E000FCFF0DE0252008F055FEE8E708F052FE8A +:10F7F000E5E790F85E204FF49671002008F0EDFFE9 +:10F80000206890F82C10294380F82C1090F82420C0 +:10F8100032EA01011CD04670418C13292BD026DC22 +:10F82000102904BF03B0BDE8F083122923D007E0FC +:10F8300040420F000C06002053E4B36E6800002025 +:10F84000C1F30010002818BFFFDF03B0BDE8F0834C +:10F85000418C1D2908BF80F82C70DCD0C1F3001149 +:10F86000002914BF80F8316080F83170D3E7152982 +:10F8700018BF1D29DBD190F85E2003B04FF00101C5 +:10F88000BDE8F043084609F094B900BF90F85F2046 +:10F890000121084609F08DF92168002DC87E7CD031 +:10F8A0004A8C3D46C2F34000002808BF47F00805D7 +:10F8B00012F0400F18BF45F04005002819BFD1F8DD +:10F8C0003C90B1F84080D1F84490B1F8488060682D +:10F8D000072107800EF046FA002160680EF039FC1F +:10F8E000294660680EF041FC15F0080F17D020681B +:10F8F000BDF800100223B0F86A2062F30B01ADF8E6 +:10F90000001090F86B00032201099DF8010061F3DB +:10F9100007108DF80100694660680EF000FC606811 +:10F920000EF0DCFA2168C0F1FE00B1F85A20A8EB15 +:10F9300002018142A8BF0146CFB2D019404544D24E +:10F9400045F0100160680EF010FC60680EF0C6FA19 +:10F950002168C0F1FE00B1F85A10A8EB0101814204 +:10F96000A8BF0146CFB260680EF0EFFB3844421CDE +:10F970002068B0F86A10436EC1F30B0151FA83F1AD +:10F9800090F86030FE4D1944AC460023E1FB05C3FE +:10F990004FEA131C6FF0240300E03CE00CFB031162 +:10F9A00080F8601090F85F00012100F095FD009054 +:10F9B000BDF800009DF80210032340EA01400190C9 +:10F9C000042201A960680EF0AAFB216891F82200C8 +:10F9D00010F0400F05D001230622613160680EF05F +:10F9E0009EFB20683A46B0F85A0000EB09016068B7 +:10F9F0000EF0E9FB2068B0F85A103944A0F85A100C +:10FA000009F0BFFC002818BFFFDF20684670867031 +:10FA100003B0BDE8F0830121FFF7A1FAF0E7D94870 +:10FA200010B50068417841B90078FF2805D0002161 +:10FA30000846FFF7D8FD002010BD09F05FF809F077 +:10FA40003EF808F007FF08F0B3FF0C2010BD2DE9C9 +:10FA5000F041CC4D0446174628680E4690F86C00DD +:10FA6000002818BFFFDF2868002F80F86E7018BFCD +:10FA7000BDE8F0812188A0F870106188A0F8861098 +:10FA8000A188A0F88810E188A0F88A1094F888115D +:10FA900080F88C1090F82F10002749B1427B00F1BC +:10FAA0000E01012A04D1497901F0C001402935D065 +:10FAB00090F8301041B1427B00F10E01012A04BFE1 +:10FAC000497911F0C00F29D000F17A00F3F794FAC8 +:10FAD0006868FF2E0178C1F380116176D0F80310B9 +:10FAE000C4F81A10B0F80700E08328681ED0C0F8E8 +:10FAF0008010E18BA0F8841000F17402511E304692 +:10FB00000DF014FF002808BFFFDF286890F873107D +:10FB100041F0020180F87310BDE8F081D0F80E10BA +:10FB2000C0F87A10418AA0F87E10D1E7C0F8807042 +:10FB3000A0F88470617E80F87310D4F81A104167C1 +:10FB4000E18BA0F87810BDE8F08170B58D4C0125EF +:10FB5000206890F82200C0F3C00038B13C22FF2199 +:10FB6000A068FFF774FF206880F86C50206890F858 +:10FB7000220010F0010F1CBFA06801884FF03C026A +:10FB800012BF01204FF6FF710020FEF717FD20681D +:10FB900080F8395070BD7B49096881F832007047A0 +:10FBA0002DE9F041774C0026206841780127354641 +:10FBB000012906D0022901D003297DD0FFDFBDE84D +:10FBC000F081817802250029418C46D0C1F34002A2 +:10FBD000002A08BF11F0010F6FD090F85F204FF09E +:10FBE00001014FF0000008F0E4FF216891F82200C5 +:10FBF000C0F34000002814BF0C20222091F85F10B1 +:10FC000008F01FFB2068457090F8330058B108F0E9 +:10FC100068F8206890F85F0010F00C0F0CBF4020CF +:10FC2000452008F077FF206890F83400002818BFBE +:10FC300008F08FFF216891F85F0091F8691010F0CB +:10FC40000C0F08BF0021962008F0F6FE09F090FB8B +:10FC5000002818BFFFDFBDE8F081C1F3001282B1B8 +:10FC600010293FD090F8330020B108F03AF8402036 +:10FC700008F050FF206890F8221011F0040F36D0E1 +:10FC800043E090F8242090F82C309A422AD1B0F822 +:10FC90004800002808BF11F0010F05D111F0020F34 +:10FCA00008BF11F0200F6FD04FF001014FF000009E +:10FCB000FFF799FC206801E041E035E0418C11F04C +:10FCC000010F04BFC1F34001002907D1B0F85A1059 +:10FCD000B0F84820914218BFBDE8F08180F831703B +:10FCE000BDE8F081BDE8F041002101207BE490F8FF +:10FCF0003710012914BF0329102646F00E0190F891 +:10FD00005E204FF0000008F054FF206890F83400A7 +:10FD1000002818BF08F01DFF0021962008F08CFE77 +:10FD200020684570BDE8F081B0F85A10B0F848007E +:10FD3000814242D0BDE8F0410121084653E4817878 +:10FD4000D9B1418C11F0010F22D080F86C7090F87D +:10FD50006E20B0F870100120FEF730FC206845706E +:10FD600008F0CCFE08F0ABFE08F074FD08F020FEB1 +:10FD7000BDE8F04103200AF07CB88178012004E05E +:10FD800053E4B36E6800002017E0BDE8F0412AE4B8 +:10FD900011F0020F04BFFFDFBDE8F081B0F85A1088 +:10FDA000B0F84000814208D001210846FFF71BFC53 +:10FDB000216803204870BDE8F081BDE8F041FFF7FD +:10FDC00082B8FFF780B810B5FE4C206890F8341068 +:10FDD00049B1383008F0CCFE18B921687F2081F88D +:10FDE000380008F0ACFE206890F8330018B108F035 +:10FDF0009BFE07F08AFF0AF02EFCA8B1206890F85D +:10FE00002210C1F3001179B14078022818BFFFDF3A +:10FE100000210120FFF7E7FB2068417800291EBF81 +:10FE200040780128FFDF10BDBDE81040FFF74BB858 +:10FE30002DE9F047E34C0F4680462168B8F1030FE7 +:10FE4000488C08BFC0F3400508D000F0010591F8C8 +:10FE50003200002818BF4FF0010901D14FF000090E +:10FE600008F00CFB0646B8F1030F0CBF4FF0020878 +:10FE70004FF0010835EA090008BFBDE8F0872068A7 +:10FE800090F8330068B10DF08FFD38700146FF28FF +:10FE900007D06068C01C0DF060FD38780DF091FD52 +:10FEA000064360680178C1F3801221680B7D9A4295 +:10FEB00008D10622C01C1531FEF792FA002808BFAF +:10FEC000012000D000203978FF2906D0C8B9206869 +:10FED00090F82D00884216D113E0A0B1616811F8A6 +:10FEE000030BC0F380100DF006FD05460DF061FE1A +:10FEF00038B128460DF0DAFB18B1102100F088FF68 +:10FF000008B1012000E00020216891F8221011F0D2 +:10FF1000040F01D0F0B11AE0CEB9AB4890F8370029 +:10FF2000002818BF404515D1616811F8030BC0F3D4 +:10FF300080100DF0E0FC04460DF03BFE38B1204689 +:10FF40000DF0B4FB18B1102100F062FF10B10120D8 +:10FF5000BDE8F0870020BDE8F0872DE9F04F994D0E +:10FF6000044683B0286800264078022818BFFFDFC7 +:10FF700028684FF07F0B90F8341049B1383008F002 +:10FF8000F7FD002804BF286880F838B008F0D7FDD6 +:10FF900068680DF009FF8046002C00F0458208F0EB +:10FFA00010FA002800F04082012400274FF0FF09DA +:10FFB000B8F1050F1ED1686890F8240000F01F000A +:10FFC000102817D9286890F8360098B18DF800905D +:10FFD00069460520FFF72CFF002800F025822868DD +:10FFE00080F8A64069682222A730C91CFEF725FACE +:10FFF00000F01ABA68680EF062F8002800F0148267 +:020000040001F9 +:100000004046DFF8C4814FF0030A062880F02182C1 +:10001000DFE800F0FCFCFC03FCFB8DF80090694677 +:100020000320FFF705FF002800F0F180296891F810 +:10003000340010B191F89C00D8B12868817801296A +:100040004DD06868042107800DF08CFE08F10E0188 +:1000500068680DF0ADFE98F80D1068680DF0C4FEEC +:100060002868B0F84020C16B68680DF0FAFE00F017 +:1000700063B99DF8000081F89C400A7881F89D20C2 +:10008000FF280FD001F19F029E310DF04FFC002898 +:1000900008BFFFDF286890F89E1041F0020180F849 +:1000A0009E100DE068680278C2F3801281F89E20ED +:1000B000D0F80320C1F89F20B0F80700A1F8A300F2 +:1000C000286800F1A50490F838007F2808BFFFDFFA +:1000D000286890F83810217080F838B0ADE790F8B3 +:1000E00022000721C0F3801938480479686869F351 +:1000F000861407800DF036FE002168680EF029F89E +:10010000214668680EF031F80623002208F10E013E +:1001100068680EF004F82868417B68680DF064FE9A +:1001200068680DF0DBFE2968B1F84020C0F1FE01DF +:100130008A42B8BF1146CFB2BA423CD9F81EC7B204 +:1001400044F0100B594668680EF00FF868680DF01F +:10015000FCFF384400F101082868B0F86A10426ECC +:10016000C1F30B0151FA82F190F86020184C0A4457 +:10017000A4460023E2FB04C319096FF0240301FB2A +:10018000032180F8601090F85F004246012100F0E2 +:10019000A3F90190BDF804009DF80610032340EA7E +:1001A00001400290042202A968680DF0B8FF594688 +:1001B00068680DF0DAFFB9F1000F0FD0D5E9001033 +:1001C000012307E0680000200C060020C86102003F +:1001D00053E4B36E062261310DF0A1FF28683A4660 +:1001E000C16B68680DF0EFFF2868A0F85A70B0F88E +:1001F00040108F420CBF0121002180F8311009F01E +:10020000C0F8002818BFFFDF96E007E021E128686A +:100210008078002840F00A8100F006B98DF800903F +:1002200068680178C1F38019D0F803100191B0F823 +:100230000700ADF8080069460520FFF7F9FD002822 +:1002400028687DD0817800297CD090F85FB0D5E90E +:100250000104D0F80F10C4F80E10B0F8131061822A +:10026000417D2175817D6175B0F81710E182B0F88C +:1002700019106180B0F81B10A180B0F81D10E1804A +:1002800000F11F0104F1080015F085FE686890F880 +:10029000241001F01F01217690F82400400984F811 +:1002A000880184F864B084F865B01BF00C0F0CBFB3 +:1002B0000021012104F130000EF0ABF92868002282 +:1002C00090F8691084F8661090F8610084F867006F +:1002D0009DF80010A868FFF7BAFB022009F0C9FDDD +:1002E000B2480DF1040B08210468686807800DF01E +:1002F00039FD002168680DF02CFF214668680DF07B +:1003000034FF0623002208F10E0168680DF007FF94 +:100310002868417B68680DF067FD494668680DF004 +:1003200070FD06230122594668680DF0F8FE09F0B9 +:1003300028F8002818BFFFDF286880F801A077E0C0 +:100340006DE0FFE76868D5F808804FF00109027892 +:1003500098F80D10C2F34012114088F80D10D0F833 +:100360000F10C8F80E10B0F81310A8F81210417D45 +:1003700088F81410817D88F81510B0F81710A8F8C7 +:100380001610B0F81910A8F80210B0F81B10A8F851 +:100390000410B0F81D10A8F8061000F11F0108F1B4 +:1003A000080015F0F8FD686890F8241001F01F01AE +:1003B00088F8181090F824000021400988F8880176 +:1003C00088F8649088F8659008F130000EF021F903 +:1003D0002868002290F8691088F8661090F861008B +:1003E00088F867009DF80010A868FFF730FB2868C0 +:1003F00080F86C4090F86E20B0F870100120FEF785 +:10040000DDF82868477008F079FB08F058FB08F021 +:1004100021FA08F0CDFA012009F02BFD08E090F850 +:100420002200C0F3001008B1012601E0FEF74BFDE9 +:10043000286890F8330018B108F076FB07F065FCE7 +:1004400096B10AF008F960B100210120FFF7CBF85E +:1004500013E0286890F82200C0F300100028E5D0CF +:10046000E2E7FEF730FD08E028688178012904D131 +:1004700090F85F10FF2007F0E4FE2868417800291B +:1004800019BF4178012903B0BDE8F08F40780328F7 +:1004900018BFFFDF03B0BDE8F08F70B5444C0646CF +:1004A0000D462068807858B107F0F2FD21680346B8 +:1004B000304691F85F202946BDE870400AF085BAC1 +:1004C00007F0E6FD21680346304691F85E20294694 +:1004D000BDE870400AF079BA78B50C460021009169 +:1004E000082804BF4FF4C87040210DD0042804BF71 +:1004F0004FF4BF70102107D0022807BF01F1180088 +:10050000042101F128000821521D02FB01062848A0 +:100510009DF80010006890F8602062F3050141F03A +:1005200040058DF8005090F85F00012829D002287E +:100530002ED004281CBF0828FFDF2FD025F0800014 +:100540008DF80000C4EB041000EB80004FF01E019A +:1005500001EB800006FB04041648844228BFFFDF3D +:100560001548A0FB0410BDF80110000960F30C0150 +:10057000ADF80110BDF800009DF8021040EA0140FE +:1005800078BD9DF8020020F0E0008DF80200D5E76C +:100590009DF8020020F0E000203004E09DF8020009 +:1005A00020F0E00040308DF80200C7E7C86102008B +:1005B00068000020C4BF0300898888880023C383A3 +:1005C000428401EBC202521EB2FBF1F1018470477A +:1005D0002DE9F04104460026D9B3552333224FF4C8 +:1005E000FA4501297DD0022900F01481032918BFA2 +:1005F000BDE8F08104F17001207B00F01F00207342 +:1006000084F889605FF0000004EB000C9CF808C0DF +:1006100003EA5C05ACEB050C0CF0FF0C0CF03305A9 +:1006200002EA9C0CAC440D180CEB1C1C0CF00F0CDB +:1006300085F814C04D7E401CAC44C0B281F819C08E +:100640000528E1D30CF0FF00252898BFBDE8F08114 +:10065000DCE0FFE704F17005802200212846FDF769 +:1006600016FFAE71EE712E736E73EE732E746E7193 +:10067000AE76EE76212085F84000492085F84100CD +:10068000FE2085F874002588702200212046FDF7A1 +:10069000FEFE2580012584F8645084F865502820EA +:1006A00084F86600002104F130000DF0B2FF1B2237 +:1006B000A4F84E20A4F85020A4F85220A4F8542006 +:1006C0004FF4A470A4F85600A4F8580065734FF4D2 +:1006D00048606080A4F8F060A4F8F260A4F8F460C8 +:1006E00000E023E0A4F8F660A4F8F86084F8FA606B +:1006F00084F8FD60A4F8066184F80461A4F8186128 +:10070000A4F81A6184F8B66184F8B76184F8C0610E +:1007100084F8C16184F88C6184F88F6184F8A861E1 +:10072000C4F8A061C4F8A461BDE8F081A4F8066132 +:1007300084F8FB606088FE490144B1FBF0F1A4F845 +:1007400090104BF68031A4F89210B4F806C0A4F8CB +:100750009860B4F89C704FEACC0C4743BCFBF0FCAB +:1007600097FBF0F70CF1010CA4F89C701FFA8CFCBD +:100770000CFB00F704F17001A4F89AC0B7F5C84F5C +:10078000C4BFACF1010CA1F82AC0B5FBF0FC0CF120 +:10079000010CA1F830C000F5802C0CF5EE3CACF15A +:1007A0000105B5FBF0FCA1F820C0CD8B05FB00FCDA +:1007B000BCFBF0F0C8830846217B01F01F012173C8 +:1007C0004676002104EB010C9CF808C003EA5C05A6 +:1007D000ACEB050C0CF0FF0C0CF0330502EA9C0CA2 +:1007E000AC4445180CEB1C1C0CF00F0C85F814C025 +:1007F000457E491CAC44C9B280F819C00529E1D333 +:100800000CF0FF00252898BFBDE8F081FFDFBDE8B0 +:10081000F08100BFB4F8B011B4F8B4316288A4F824 +:100820009860B4F89CC0DB000CFB02FCB3FBF1F356 +:100830009CFBF1FC5B1CA4F89CC09BB203FB01FC7D +:1008400004F17000A4F89A30BCF5C84FC4BF5B1E19 +:100850004385B5FBF1F35B1C0386438C01EBC303BB +:100860005B1EB3FBF1F30384C38B5A43B2FBF1F17C +:10087000C183BDE8F0812DE9F04104460025A1B314 +:1008800055234FF4FA464FF0330C01297DD002294D +:1008900000F0E080032918BFBDE8F08104F170008A +:1008A000217B01F01F01217384F889500021621817 +:1008B000127A03EA5205521BD2B202F033050CEA57 +:1008C00092022A44451802EB121202F00F022A7516 +:1008D000457E491C2A44C9B242760529E7D3D0B2E5 +:1008E000252898BFBDE8F081B1E0FFE704F170066C +:1008F000802200213046FDF7CAFDB571F5713573D0 +:100900007573F57335747571B576F576212086F8B3 +:100910004000492086F84100FE2086F874002688B1 +:10092000702200212046FDF7B2FD2680012684F8C2 +:10093000646084F86560282084F86600002104F172 +:1009400030000DF066FE1B22A4F84E20A4F85020C3 +:10095000A4F85220A4F854204FF4A470A4F8560030 +:10096000A4F858006673A4F8F850202084F8FA0020 +:1009700084F8F050C4F8F45084F8245184F82551D8 +:1009800084F82E5184F82F5100E005E084F81451CA +:1009900084F82051BDE8F081618865480844B0FBC7 +:1009A000F1F0A4F890004BF68030A4F89200E288B1 +:1009B000A4F89850B4F89C70D2004F43B2FBF1F207 +:1009C00097FBF1F7521CA4F89C7092B202FB01F75E +:1009D00004F17000A4F89A20B7F5C84FC4BF521EA6 +:1009E0004285B6FBF1F2521C028601F5802202F527 +:1009F000EE32561EB6FBF1F20284C68B06FB01F204 +:100A0000B2FBF1F1C1830146207B00F01F0020738F +:100A10004D7600202218127A03EA5205521BD2B2F8 +:100A200002F033050CEA92022A440D1802EB12126E +:100A300002F00F022A754D7E401C2A44C0B24A764D +:100A40000528E7D3D0B2252898BFBDE8F081FFDFA5 +:100A5000BDE8F081D0F81811628804F1700348896C +:100A6000C989A4F89850B4F89CC0C9000CFB02FCDA +:100A7000B1FBF0F19CFBF0FC491CA4F89CC089B2CE +:100A800001FB00FCA4F89A10BCF5C84FC4BF491E76 +:100A90005985B6FBF0F1491C1986598C00EBC10150 +:100AA000491EB1FBF0F11984D98B5143B1FBF0F031 +:100AB000D883BDE8F0812DE9F003447E0CB1252CEC +:100AC00003D9BDE8F00312207047002A02BF0020BE +:100AD000BDE8F003704791F80DC01F260123154DA6 +:100AE0004FF00008BCF1000F7AD0BCF1010F1EBF1F +:100AF0001F20BDE8F0037047B0F800C00A7C8F7B70 +:100B000091F80F907A404F7C87EA090742EA072262 +:100B100082EA0C0C5FF000070CF0FF0999FAA9F9C2 +:100B20004FEA1C2C4FEA19699CFAACFC04E0000067 +:100B3000FFDB050053E4B36E4FEA1C6C49EA0C2C52 +:100B40000CEB0C1C7F1C9444FFB21FFA8CFC032F8F +:100B5000E2D38CEA020CFB4F0022ECFB0572120977 +:100B60006FF0240502FB05C2D2B201EBD2078276F8 +:100B700002F007053F7A03FA05F52F4218BFC27647 +:100B80007ED104FB0CF2120C521CD2B25FF00004B6 +:100B900000EB040C9CF814C094453CBFA2EB0C0283 +:100BA000D2B212D30D194FF0000C2D7A03FA0CF7C4 +:100BB0003D421CBF521ED2B2002A69D00CF1010C7A +:100BC0000CF0FF0CBCF1080FF0D304F1010C0CF099 +:100BD000FF04052CDCD33046BDE8F0037047FFE787 +:100BE00090F81AC00C7E474604FB02C2D54C4FF069 +:100BF000000CE2FB054C4FEA1C1C6FF024040CFBBC +:100C00000422D2B201EBD204827602F0070C247ADD +:100C100003FA0CFC14EA0C0F1FBFC2764046BDE875 +:100C2000F003704790F819C0B2FBFCF40CFB1422DF +:100C3000521CD2B25FF0000400EB040C9CF814C00C +:100C400094453CBFA2EB0C02D2B212D30D194FF067 +:100C5000000C2D7A03FA0CF815EA080F1CBF521E7F +:100C6000D2B272B10CF1010C0CF0FF0CBCF1080F08 +:100C7000F0D304F1010C0CF0FF04052CDCD3AAE73F +:100C800009E00CEBC401C1763846BDE8F0037047BB +:100C90000CEBC401C1764046BDE8F0037047AA4A98 +:100CA000016812681140A94A126811430160704737 +:100CB00030B4A749A44B00244FF0010C0A78521C11 +:100CC000D2B20A70202A08BF0C700D781A680CFA8C +:100CD00005F52A42F2D0097802680CFA01F1514078 +:100CE000016030BC704770B46FF01F02010C02EA63 +:100CF00090251F23A1F5AA4054381CBFA1F5AA4096 +:100D0000B0F1550009D0A1F52850AA381EBFA1F5B1 +:100D10002A40B0F1AA00012000D100204FF0000CC1 +:100D2000624601248CEA0106F6431643B6F1FF3F02 +:100D300011D005F001064FEA5C0C4CEAC63C03F00A +:100D4000010652086D085B08641C42EAC632162C84 +:100D5000E8DD70BC704770BC0020704790F804C09C +:100D60003CF01F011CBF0020704730B401785522B1 +:100D700002EA5103C91AC9B201F03304332303EA6A +:100D800091012144447801EB111102EA5405641BDE +:100D9000E4B204F0330503EA94042C4404EB141485 +:100DA00001F00F0104F00F040C448178C07802EACE +:100DB0005105491BC9B201F0330503EA91012944E9 +:100DC00001EB111101F00F01214402EA5004001B54 +:100DD000C0B200F0330403EA9000204400EB10108E +:100DE00000F00F00014402EA5C00ACEB0000C0B26E +:100DF00000F0330203EA9000104400EB101000F002 +:100E00000F00084401288CBF0120002030BC70472F +:100E10000A000ED00123012A0BDB491EC9B210F8CB +:100E200001C0BCF1000F01D0002070475B1C934251 +:100E3000F3DD01207047002A08BF70471144401EAF +:100E400012F0010F03D011F8013D00F8013F5208E4 +:100E500008BF704711F8013C437011F8023D00F8DB +:100E6000023F521EF6D1704770B58CB000F11004ED +:100E70001D4616460DF1FF3C5FF0080014F8012CEA +:100E80008CF8012014F8022D0CF8022F401EF5D129 +:100E900001F1100C6C460DF10F0108201CF8012C1B +:100EA0004A701CF8022D01F8022F401EF6D1204690 +:100EB00013F01CFB7EB16A1E04F130005FF00801E4 +:100EC00010F8013C537010F8023D02F8023F491E31 +:100ED000F6D10CB070BD08982860099868600A982F +:100EE000A8600B98E8600CB070BD38B505460C469C +:100EF000684607F03DFE002808BF38BD9DF9002078 +:100F00002272E07E607294F90A100020511A48BFE4 +:100F1000494295F82D308B42C8BF38BDFF2B08BF22 +:100F200038BDE17A491CC9B2E17295F82E30994278 +:100F300003D8A17A7F2918BF38BDA2720020E072C1 +:100F4000012038BD53E4B36E04620200086202005F +:100F5000740000200C2818BF0B2810D00D2818BFD3 +:100F60001F280CD0202818BF212808D0222818BFFD +:100F7000232804D024281EBF2628002070474FF0C5 +:100F8000010070470C2963D2DFE801F006090E1357 +:100F9000161B323C415C484E002A5BD058E0072AC1 +:100FA00018BF082A56D053E00C2A18BF0B2A51D07C +:100FB0004EE00D2A4ED04BE0A2F10F000C2849D98B +:100FC00046E023B1A2F110000B2843D940E0122AD9 +:100FD00018BF112A3ED090F8380020B1122A37D31A +:100FE0001A2A37D934E0162A32D31A2A32D92FE0F6 +:100FF000A2F10F0103292DD990F8380008B31B2A5C +:1010000028D925E0002B08BF042A21D122E013B102 +:10101000062A1FD01CE0012A1AD11BE01C2A1CBF83 +:101020001D2A1E2A16D013E01F2A18BF202A11D00D +:10103000212A18BF222A0DD0232A1CBF242A262A9F +:1010400008D005E013B10E2A04D001E0052A01D032 +:1010500000207047012070472DE9F0410D460446FD +:10106000866805F02FFA58B905F07EF840F236711F +:1010700004F061FDA060204605F024FA0028F3D0BA +:1010800095B13046A16805F067FD00280CDD2844C5 +:10109000401EB0FBF5F707FB05F1304604F04BFDB1 +:1010A000A0603846BDE8F0810020BDE8F08170B551 +:1010B0000446904228BF70BD101B64280BD325182E +:1010C0008D4206D8042105F07AFD00281CBF284671 +:1010D00070BD204670BD6420F1E711F00C0F13D0F5 +:1010E00001F0040100290DBF4022102296214FF487 +:1010F000167101F5BC71A0EB010388428CBF93FB14 +:10110000F2F0002080B27047022919BF6FF00D0184 +:1011100001EBD0006FF00E0101EB9000F2E7084404 +:1011200018449830002A14BF042100210844704755 +:1011300010B4002A14BF4FF429624FF4A472002B9C +:1011400019BF4FF429634FF0080C4FF4A4734FF00C +:10115000010C00280CBF0124002491F866001CF04B +:101160000C0F08BF0020D11808449830002C14BF81 +:1011700004210021084410BC704700280CBF012343 +:10118000002391F86600002BA0F6482000F50050DF +:1011900018BF04231844496A81422CBF0120002053 +:1011A00012F00C0118BF012131EA000014BF002029 +:1011B0000120704710B413680B66137813F00C030A +:1011C00018BF0123527812F00C0218BF012253EA13 +:1011D000020C04BF10BC7047002B0CBF4FF4A4736B +:1011E0004FF42963002A19BF4FF429624FF0080C0D +:1011F0004FF4A4724FF0010C00280CBF012400240E +:1012000091F866001CF00C0F08BF00201A4410442F +:101210009830002C14BF0422002210444A6A8242F3 +:1012200024BF10BC704791F860004FF0030230F00B +:101230000C0381F8603091F8610020F00C0081F817 +:10124000610008BF81F86020002808BF81F8612094 +:1012500010BC704710F0010F1CBF0120704710F048 +:10126000020F1CBF0220704710F0040018BF0820B6 +:1012700070472DE9F0470446174689464FF00108AC +:1012800008460DF0FAF8054648460DF0FAF810F059 +:10129000010F18BF012624D015F0010F18BF01233C +:1012A0002AD000BF56EA030108BF4FF0000810F033 +:1012B000070F08BF002615F0070F08BF002394F89A +:1012C0006400B0420CBF00203046387094F86510BE +:1012D000994208BF00237B70002808BF002B25D14E +:1012E00015E010F0020F18BF0226D5D110F0040F40 +:1012F00014BF08260026CFE715F0020F18BF0223FF +:10130000D0D115F0040F14BF08230023CAE74846C4 +:101310000DF0BDF8B4F87010401A00B247F6FE7137 +:10132000884201DC002801DC4FF0000816B1082ECD +:101330000CD018E094F86400012818BF022812D0DD +:1013400004281EBF0828FFDF032D0CD194F8C0012C +:1013500048B1B4F8C401012894F8640006D0082804 +:1013600001D0082038704046BDE8F087042818BF37 +:101370000420F7D1F5E7012814BF0228704710F0C8 +:101380000C0018BF0420704738B4CBB2C1F3072C4F +:10139000C1B2C0F30724012B07D0022B09D0042BC4 +:1013A00008BFBCF1040F2DD006E0BCF1010F03D142 +:1013B00028E0BCF1020F25D0012906D0022907D070 +:1013C000042908BF042C1DD004E0012C02D119E02F +:1013D000022C17D001EA0C0161F3070204EA0301B1 +:1013E00061F30F22D1B211F0020F18BF022310D007 +:1013F000C2F307218DF8003011F0020F18BF02214F +:101400001BD111E0214003EA0C03194061F30702EC +:10141000E6E711F0010F18BF0123E9D111F0040F25 +:1014200014BF08230023E3E711F0010F18BF0121C7 +:1014300003D111F0040118BF08218DF80110082B09 +:1014400001BF000C012804208DF80000BDF8000049 +:1014500038BC70474FF0000C082902D0042909D08D +:1014600011E001280FD10420907082F803C013808E +:1014700001207047012806D00820907082F803C030 +:1014800013800120704700207047162A10D12A22AD +:101490000C2818BF0D280FD04FF0230C1F280DD09B +:1014A00031B10878012818BF002805D0162805D0CA +:1014B00000207047012070471A70FBE783F800C0D6 +:1014C000F8E7012908D002290BD0042912BF082906 +:1014D00040F6A660704707E0002804BF40F2E240F3 +:1014E000704740F6C410704700B5FFDF40F2E2409D +:1014F00000BD00000178406829B190F82C1190F8E7 +:101500008C0038B901E001F0BDBD19B1042901D04A +:10151000012070470020704770B50C460546062133 +:1015200002F0C4FC606008B1002006E007212846F4 +:1015300002F0BCFC606018B101202070002070BD7A +:10154000022070BD2DE9FC470C4606466946FFF7B0 +:10155000E3FF00287DD19DF8000050B1FDF7EEF8C3 +:10156000B0427CD0214630460AF008FC002873D1F6 +:101570002DE00DF097F9B04271D02146304612F0BF +:10158000B6FA002868D1019D95F8F00022E001200C +:1015900000E00020804695F839004FF0010A4FF036 +:1015A0000009F0B195F83A0080071AD584F8019047 +:1015B00084F800A084F80490E68095F83B1021722E +:1015C000A98F6181E98FA18185F8399044E0019D5F +:1015D00095F82C0170350028DBD1287F0028D8D061 +:1015E000D5E7304602F0A5FD070000D1FFDF384601 +:1015F00001F0B5FF40B184F801900F212170E68021 +:10160000208184F804A027E0304602F080FD070026 +:1016100000D1FFDFB8F1000F21D0384601F0F7FF0D +:10162000B8B19DF8000038B90198D0F81801418888 +:10163000B14201D180F80090304607F00DFF84F8E8 +:1016400001900C21217084F80490E680697F21725A +:1016500000E004E085F81C900120BDE8FC87002034 +:10166000FBE71CB56946FFF757FF00B1FFDF68468F +:1016700001F014FDFE4900208968A1F8F2001CBDAC +:101680002DE9FC4104460E46062002F0B7FB054654 +:10169000072002F0B3FB2844C7B20025A8463E4409 +:1016A00017E02088401C80B22080B04202D3404620 +:1016B000A4F8008080B2B84204D3B04202D2002025 +:1016C000BDE8FC816946FFF727FF0028F8D06D1CB4 +:1016D000EDB2AE42E5D84FF6FF7020801220EFE762 +:1016E00038B54FF6FF70ADF800000DE00621BDF8EB +:1016F000000002F0EDFB04460721BDF8000002F0F7 +:10170000E7FB0CB100B1FFDF00216846FFF7B8FF2F +:101710000028EBD038BD70B507F00CFF0BF034FF9C +:10172000D44C4FF6FF76002526836683D2A0257021 +:1017300001680079A4F14002657042F8421FA11CC3 +:101740001071601C12F0EFFA1B2020814FF4A4717D +:101750006181A081E18107212177617703212174D3 +:10176000042262746082A082A4F13E00E1820570CE +:101770004680BF480C300570A4F11000057046800B +:1017800084F8205070BD70B5B94C16460D466060A7 +:10179000217007F047FEFFF7A3FFFFF7BCFF20789B +:1017A0000FF0BDFFB6480DF0D0F92178606812F057 +:1017B0005FFA20780BF0DCF8284608F0AFFEB0485E +:1017C000FCF7C7FF217860680AF0B2FB3146207849 +:1017D00012F024FDBDE870400BF0D6BE10B5012418 +:1017E0000AB1002010BD21B1012903D000242046F8 +:1017F00010BD02210CF024FDF9E710B50378044672 +:10180000002B406813460A46014609D05FF00100EC +:10181000FFF78EFC6168496A884203D9012010BD38 +:101820000020F5E7002010BD2DE9F04117468A7829 +:101830001E46804642B11546C87838B1044669074D +:1018400006D52AB1012104E00725F5E70724F6E7CC +:101850000021620702D508B1012000E0002001420A +:1018600006D0012211464046FFF7C7FF98B93DE078 +:1018700051B1002201214046FFF7BFFF58B9600770 +:1018800034D50122114620E060B1012200214046FA +:10189000FFF7B3FF10B10920BDE8F081680725D537 +:1018A000012206E068074FEA44700AD5002814DBDD +:1018B000002201214046FFF7A0FFB8B125F0040542 +:1018C00014E0002812DA012200214046FFF795FFBC +:1018D00060B100BF24F0040408E001221146404634 +:1018E000FFF78BFF10B125F00405F3E73D7034706E +:1018F0000020D1E770B58AB0044600886946FFF73A +:101900000BFE002806D1A08830B1012804D002289F +:1019100002D012200AB070BD04AB03AA214668466B +:10192000FFF782FF0500F5D19DF800100120002689 +:101930000029019906D081F8C101019991F80C1292 +:10194000B1BB2DE081F82F01019991F8561139B9F9 +:10195000019991F82E1119B9019991F8971009B1CF +:101960003A2519E00199059681F82E01019A9DF812 +:101970000C0082F83001019B9DF8102083F8312182 +:10198000A388019CA4F832318DF814008DF815203D +:1019900005AA0020FFF70EFC019880F82F6126E0D1 +:1019A000019991F8C01119B9019991F8971009B1ED +:1019B0003A2519E00199059681F8C00101989DF832 +:1019C0000C2080F8C221019B9DF8100083F8C30110 +:1019D000A388019CA4F8C4318DF814208DF815005B +:1019E00005AA0120FFF7E6FB019880F8C1612846AF +:1019F00090E710B504460020A17801B90120E278F3 +:101A00000AB940F0020001F058FB002803D120463B +:101A1000BDE810406EE710BD70B5044691F8650052 +:101A200091F866300D4610F00C0F00D1002321898B +:101A3000A088FFF774FB696A814229D2401A401CD2 +:101A4000A1884008091A8AB2A2802189081A208137 +:101A5000668895F864101046FFF73FFB864200D277 +:101A600030466080E68895F8651020890AE000001D +:101A70007800002018080020FFFFFFFF1F00000073 +:101A8000D8060020FFF729FB864200D23046E080CE +:101A900070BDF0B585B00D46064603A9FFF73CFDC5 +:101AA00000282DD19DF80C0060B300220499FB2082 +:101AB000B1F84E30FB2B00D30346B1F85040FB2069 +:101AC000FB2C00D30446DFF85CC59CE88110009035 +:101AD0000197CDF808C0ADF80230ADF80640684671 +:101AE000FFF79AFF6E80BDF80400E880BDF808009B +:101AF0006881BDF80200A880BDF80600288100209A +:101B000005B0F0BD0122D1E72DE9F04186B00446D1 +:101B100000886946FFF700FD002876D12189E0881A +:101B200001F0E4FA002870D1A188608801F0DEFAA3 +:101B300000286AD12189E08801F0CFFA002864D119 +:101B4000A188608801F0C9FA07005ED1208802A947 +:101B5000FFF79FFF00B1FFDFBDF81010628809207A +:101B6000914252D3BDF80C10E28891424DD3BDF89A +:101B70001210BDF80E2023891144A2881A44914204 +:101B800043D39DF80010019D4FF00008012640F658 +:101B9000480041B185F8B761019991F8F81105F550 +:101BA000DB7541B91AE085F82561019991F84A1170 +:101BB00005F5927509B13A2724E0E18869806188CA +:101BC000E9802189814200D30146A980A188814210 +:101BD00000D208462881012201990FE0E18869803E +:101BE0006188E9802189814200D30146A980A188CA +:101BF000814200D208462881019900222846FFF739 +:101C00000BFF2E7085F80180384606B044E67BE76E +:101C100070B504460CF0FCFDB0B12078182811D145 +:101C2000207901280ED1E088062102F03FF9040056 +:101C300008D0208807F010FC2088062102F048F91F +:101C400000B1FFDF012070BDF74D28780028FAD0E1 +:101C5000002666701420207020223146201DFCF7DB +:101C600016FC022020712E70ECE710B50446FCF73C +:101C7000DBFC002813D0207817280FD1207968B119 +:101C8000E088072102F012F940B1008807F0E4FB78 +:101C9000E088072102F01CF900B1FFDF012010BD30 +:101CA0002DE9F0475FEA000800D1FFDFDE4802219E +:101CB0001A308146FFF7E4FC00B1FFDFDA4C062062 +:101CC000678B02F09BF80546072002F097F828443E +:101CD000C5B2681CC6B2608BB04203D14046FFF764 +:101CE000C4FF58B9608BA84203D14046FFF790FF6C +:101CF00020B9608B4146FFF725FC38B1404601F022 +:101D000003FA0028E7D10120BDE8F0870221484608 +:101D1000FFF7B6FC10B9608BB842DCD14046BDE895 +:101D2000F04712F0C1BA10B501F053F908B10C2018 +:101D300010BD0BF07DFC002010BD10B504460078EE +:101D400018B1012801D0122010BD01F053F920B1C3 +:101D50000BF0C0FD08B10C2010BD207801F013F984 +:101D6000E21D04F11703611CBDE810400BF0DABC62 +:101D700010B5044601F02DF908B10C2010BD2078F3 +:101D800028B1012803D0FF280BD0122010BD01F08C +:101D9000FAF8611C0BF00CFC08B1002010BD072004 +:101DA00010BD01200BF03EFCF7E710B50BF095FDE0 +:101DB00008B1002010BD302010BD10B5044601F060 +:101DC00019F908B10C2010BD20460BF080FD002051 +:101DD00010BD10B501F00EF920B10BF07BFD08B17C +:101DE0000C2010BD0BF0F6FC002010BDFF2181700F +:101DF0004FF6FF7181808D4949680A7882718A881F +:101E000002814988418101214170002070477CB5E1 +:101E10000025022A19D015DC12F10C0F15D009DCAF +:101E200012F1280F11D012F1140F0ED012F1100F71 +:101E300011D10AE012F1080F07D012F1040F04D0FB +:101E40004AB902E0D31E052B05D8012806D0022886 +:101E500008D003280AD0122528467CBD1046FDF77D +:101E600013F8F9E710460CF06BFEF5E70846144648 +:101E70006946FFF751FB08B10225EDE79DF8000028 +:101E80000198002580F86740E6E710B51346012267 +:101E9000FEF7EAFF002010BD10B5044610F02FFA3F +:101EA000052804D020460FF029FC002010BD0C208E +:101EB00010BD10B5044601F09DF808B10C2010BD0E +:101EC0002146002007F037FB002010BD10B5044666 +:101ED0000FF0A3FC50B108F0A6FD38B1207808F04F +:101EE00029FB20780DF04DF9002010BD0C2010BD0D +:101EF00010B5044601F07EF808B10C2010BD214653 +:101F0000012007F018FB002010BD38B504464FF63D +:101F1000FF70ADF80000A079E179884216D02079F1 +:101F2000FCF7E3FA90B16079FCF7DFFA70B10022B8 +:101F3000A079114612F0A0FD40B90022E0791146C7 +:101F400012F09AFD10B9207A072801D9122038BD65 +:101F500008F076FD60B910F0D2F948B90021684662 +:101F6000FFF78EFB20B1204606F044F9002038BD73 +:101F70000C2038BD2DE9FC41817805461A2925D071 +:101F80000EDC16292ED2DFE801F02D2D2D2D2D216E +:101F90002D2D2D2D2D2D2D2D2D2D2D2D2D21212195 +:101FA0002A291FD00BDCA1F11E010C291AD2DFE86F +:101FB00001F019191919191919191919190D3A399D +:101FC00004290FD2DFE801F00E020E022888B0F5D6 +:101FD000706F07D201276946FFF79EFA20B10220F1 +:101FE000BDE8FC811220FBE79DF8000000F0D2FF65 +:101FF000019C10B104F58A7401E004F5C6749DF8E3 +:10200000000000F0C7FF019E10B106F2151601E0B6 +:1020100006F28D166846FFF76DFA08B1207838B1E0 +:102020000C20DDE70C620200180800207800002078 +:102030002770A8783070684601F030F80020CFE7AC +:102040007CB50D466946FFF767FA002618B12E6089 +:102050002E7102207CBD9DF8000000F09BFF019CCA +:102060009DF80000703400F095FF019884F84260FC +:1020700081682960017B297194F842100029F5D10B +:1020800000207CBD10B5044600F0B4FF20B10BF079 +:1020900021FC08B10C2010BD207800F074FFE2791B +:1020A000611C0BF093FD08B1002010BD022010BD93 +:1020B00010B5886E60B1002241F8682F0120CA7106 +:1020C0008979884012F0CCFC002800D01F2010BD78 +:1020D0000C2010BD1CB50C466946FFF71DFA002800 +:1020E00009D19DF8000000280198B0F8700000D0D8 +:1020F000401C208000201CBD1CB504460088694699 +:10210000FFF70AFA08B102201CBD606828B1DDE9BA +:102110000001224601F04CF81CBDDDE90001FFF78B +:10212000C7FF1CBD70B51C460D4618B1012801D073 +:10213000122070BD1946104601F078F830B12146E2 +:10214000284601F07DF808B1002070BD302070BD38 +:1021500070B5044600780E46012804D018B1022854 +:1021600001D0032840D1607828B1012803D002288B +:1021700001D0032838D1E07B10B9A078012833D1F1 +:10218000A07830F005012FD110F0050F2CD0628916 +:10219000E188E0783346FFF7C5FF002825D1A07815 +:1021A00005281DD16589A289218920793346FFF749 +:1021B000B9FF002819D1012004EB40014A891544D8 +:1021C0002218D378927893420ED1CA8889888A429D +:1021D0000AD1401CC0B20228EED3E088A84203D343 +:1021E000A07B08B1072801D9122070BD002070BD66 +:1021F00010B586B0044600F0E1FE10B10C2006B028 +:1022000010BD022104F10A0001F02FF8A0788DF82A +:102210000800A0788DF8000060788DF80400207820 +:102220008DF80300A07B8DF80500E07B00B1012054 +:102230008DF80600A078C10717D0E07801F00CF8FF +:102240008DF80100E088ADF80A006089ADF80C0057 +:10225000A078400716D5207900F0FEFF8DF8020027 +:102260002089ADF80E00A0890AE040070AD5E07881 +:1022700000F0F2FF8DF80200E088ADF80E006089F2 +:10228000ADF8100002A80FF0D4FA0028B7D16846C4 +:102290000CF07CFFB3E710B504460121FFF758FFAF +:1022A000002803D12046BDE81040A1E710BD027808 +:1022B000012A01D0BAB118E042783AB1012A05D01A +:1022C000022A12D189B1818879B100E059B14188DF +:1022D00049B1808838B101EB8101490000EB8000F1 +:1022E000B1EB002F01D2002070471220704770B56B +:1022F000044600780D46012809D010F000F80528A2 +:1023000003D00FF0A6F9002800D00C2070BD0CF00F +:102310000AFE88B10CF01CFE0CF018FF0028F5D165 +:1023200025B160780CF0ACFE0028EFD1A188608860 +:10233000BDE870400FF0A3BA122070BD10B504467E +:102340000121FFF7B4FF002804D12046BDE810406A +:102350000121CCE710BDF0B5871FDDE9056540F62A +:102360007B44A74213D28F1FA74210D288420ED8B7 +:10237000B2F5FA7F0BD2A3F10A00241FA04206D2C5 +:10238000521C4A43B2EB830F01DAAE4201D900205E +:10239000F0BD0120F0BD2DE9FC47477A894604468F +:1023A00017F0050F7ED0F8087CD194F83A0008B9F0 +:1023B000012F77D10025A8462E46F90789F0010A9A +:1023C00019D0208A514600F031FFE8B360895146A8 +:1023D00000F036FFC0B3208A6189884262D8A18E9E +:1023E000E08DCDE90001238D628CA18BE08AFFF79F +:1023F000B2FF48B30125B8070ED504EB4500828E25 +:10240000C18DCDE90012038D428C818BC08AFFF70C +:10241000A2FFC8B1A8466D1C78071ED504EB45067F +:102420005146308A00F002FF70B17089514600F0C9 +:1024300007FF48B1308A7189884253D8B18EF08D38 +:10244000CDE90001338D00E00BE0728CB18BF08A96 +:10245000FFF781FF28B12E466D1CB9F1000F03D0A4 +:1024600030E03020BDE8FC87F80707D0780705D5B5 +:1024700004EB460160894989884233D1228A0121CF +:102480001BE0414503D004EB4100008A024404EB09 +:102490004100C38A868AB34224D1838B468BB342E0 +:1024A00020D100E01EE0438C068CB3421AD1038D8C +:1024B000C08C834216D1491CC9B2A942E1D36089BC +:1024C00090420FD3207810B101280BD102E0A07800 +:1024D0000028F9D1607838B1012805D0022803D04E +:1024E000032801D01220BDE70020BBE7002152E7FE +:1024F0000178C90702D0406811F0A9BE11F076BE7C +:1025000010B50078012800D00020FCF7B8FC0020AE +:1025100010BD2DE9F0478EB00D46AFF6A422D2E9EA +:102520000092014690462846FFF735FF06000CD181 +:1025300000F044FD40B9FE4F387828B90CF0B2F9EC +:10254000A0F57F41FF3903D00C200EB0BDE8F08725 +:10255000032105F1100000F088FEF54809AA3E3875 +:102560000990F4480A90F248062110380B900CA804 +:1025700001F06AFC040037D00021FEF77CF904F179 +:1025800030017B8ABA8ACB830A84797C0091BA466F +:102590003B7CBA8A798A208801F044FD00B1FFDFD4 +:1025A000208806F058FF218804F10E0000F02CFD71 +:1025B000E1A004F1120700680590032105A804F0CA +:1025C0006DFF002005A90A5C3A54401CC0B20328E4 +:1025D000F9D3A88B6080688CA080288DE080687A11 +:1025E000410703D508270AE00920AEE7C10701D05B +:1025F000012704E0800701D5022700E000273A46C2 +:10260000BAF8160011460FF0CFF90146A062204635 +:102610000FF0D8F93A4621460020FEF7AEFD00B98A +:102620000926C34A21461C320020FEF7C3FD0027BD +:1026300084F8767084F87770A87800F0A4FC60764F +:10264000D5F80300C4F81A00B5F80700E083C4F811 +:10265000089084F80C80012084F8200101468DF850 +:102660000070684604F01AFF9DF8000000F00701B2 +:10267000C0F3C1021144C0F3401008448DF80000BB +:10268000401D2076092801D20830207601212046FD +:10269000FEF7F1F868780CF051FCEEBBA9782878C9 +:1026A000EA1C0CF01EFC48B10CF052FCA97828780A +:1026B000EA1C0CF0BFFC060002D052E0122650E0EB +:1026C000687A00F005010020CA0700D001208A07BF +:1026D00001D540F00200490701D540F008000CF098 +:1026E000E9FB06003DD1214603200CF0CDFC06009D +:1026F00037D10CF0D2FC060033D1697A01F0050124 +:102700008DF80810697AC90708D06889ADF80A0001 +:10271000288AADF80C0000E023E00120697A8A07DE +:1027200000D5401C490707D505EB40004189ADF8AD +:102730000E10008AADF8100002A80FF07AF80646D5 +:1027400095F83A0000B101200CF0C6FB4EB90CF030 +:10275000FDFC060005D1A98F20460FF00BF80600FE +:1027600008D0208806F078FE2088062101F0B0FB12 +:1027700000B1FFDF3046E8E601460020C9E638B583 +:102780006B48007878B90FF0BAFD052805D00CF039 +:1027900089F8A0F57F41FF3905D068460FF0B3F8FE +:1027A000040002D00CE00C2038BD0098008806F030 +:1027B00053FE00980621008801F08AFB00B1FFDF7C +:1027C000204638BD1CB582894189CDE900120389B4 +:1027D000C28881884088FFF7BEFD08B100201CBD7B +:1027E00030201CBD70B50546FFF7ECFF00280ED168 +:1027F0002888062101F05AFB040007D000F042FCB3 +:1028000020B1D4F81801017831B901E0022070BD7F +:10281000D4F86411097809B13A2070BD052181719D +:10282000D4F8181100200881D4F81811A88848811C +:10283000D4F81811E8888881D4F818112889C8813B +:10284000D4F81801028941898A4204D88279082A79 +:1028500001D88A4201D3122070BD29884180D4F862 +:10286000181102200870002070BD3EB50446FEF726 +:1028700075FAB0B12E480125A0F1400245702368D9 +:1028800042F8423F237900211371417069460620C6 +:1028900001F095FA00B1FFDF684601F06EFA10B161 +:1028A0000EE012203EBDBDF80440029880F8205191 +:1028B000684601F062FA18B9BDF80400A042F4D1EC +:1028C00000203EBD70B505460088062101F0EEFAF5 +:1028D000040007D000F0D6FB20B1D4F81811087816 +:1028E00030B901E0022070BDD4F86401007808B16D +:1028F0003A2070BDB020005D10F0010F22D0D5F855 +:1029000002004860D5F806008860D4F8180169898B +:1029100010228181D4F8180105F10C010E3004F564 +:102920008C74FBF78AFD216803200870288805E075 +:1029300018080020840000201122330021684880FC +:10294000002070BD0C2070BD38B504460078EF281B +:102950004DD86088ADF80000009800F097FC88B36F +:102960006188080708D4D4E9012082423FD8202A90 +:102970003DD3B0F5804F3AD8207B18B3072836D81E +:10298000607B28B1012803D0022801D003282ED172 +:102990004A0703D4022801D0032805D1A07B08B13F +:1029A000012824D1480707D4607D28B1012803D02D +:1029B000022801D003281AD1C806E07D03D50128DA +:1029C00015D110E013E0012801D003280FD1C8066B +:1029D00009D4607E012803D0022801D0032806D143 +:1029E000A07E0F2803D8E07E18B1012801D0122064 +:1029F00038BD002038BDF8B514460D46064608F02F +:102A00001FF808B10C20F8BD3046FFF79DFF0028E5 +:102A1000F9D1FCF73EFA2870B07554B9FF208DF853 +:102A2000000069460020FCF71EFA69460020FCF70A +:102A30000EFA3046BDE8F840FCF752B90022DAE75A +:102A40000078C10801D012207047FA4981F82000AF +:102A50000020704710B504460078C00704D1608894 +:102A600010B1FCF7D7F980B12078618800F001023D +:102A7000607800F02FFC002806D1FCF7B3F901467E +:102A80006088884203D9072010BD122010BD6168FC +:102A9000FCF7E9F9002010BD10B504460078C00726 +:102AA00004D1608810B1FBF78AFE70B1207861888C +:102AB00000F00102607800F00DFC002804D160886D +:102AC0006168FCF7C4F9002010BD122010BD7CB570 +:102AD000044640784225012808D8A078FBF767FE15 +:102AE00020B120781225012802D090B128467CBD63 +:102AF000FCF7DBF920B1A0880028F7D08028F5D8B2 +:102B0000FCF7DAF960B160780028EFD0207801286E +:102B100008D006F0C3FD044607F05DFC00287FD016 +:102B20000C207CBDFBF7F5FF10B9FCF7B7F990B3AB +:102B300007F086FF0028F3D1FBF700FEA0F57F41E8 +:102B4000FF39EDD1FCF707F8A68842F21070464332 +:102B5000A079FCF770F9FBF739FEF8B100220721E4 +:102B600001A801F071F9040058D0B3480021846035 +:102B70002046FDF72DFD2046FCF732FDAD4D04F15A +:102B800030006A8AA98AC2830184FBF726FE60B1FD +:102B9000E88A01210DE0FFE712207CBD31460020CC +:102BA00007F0CBFC88B3FFDF44E0FCF787F9014670 +:102BB000E88A07F091FD0146A0620022204606F057 +:102BC00070FDFBF70AFE38B9FCF778F9024621469A +:102BD0000120FEF7D2FAD0B1964A21461C320120DC +:102BE000FEF7E8FA687C00902B7CAA8A698A208824 +:102BF00001F018FA00B1FFDF208806F02CFC314606 +:102C0000204607F09AFC00B1FFDF13E008E007213F +:102C1000BDF8040001F05CF900B1FFDF09207CBDC4 +:102C200044B1208806F018FC2088072101F050F9F3 +:102C300000B1FFDF00207CBD002148E770B50D46E4 +:102C4000072101F033F9040003D094F88F0110B18B +:102C50000AE0022070BD94F87D00142801D01528E8 +:102C600002D194F8DC0108B10C2070BD1022294675 +:102C700004F5C870FBF7E1FB012084F88F01002008 +:102C800070BD10B5072101F011F918B190F88F113E +:102C900011B107E0022010BD90F87D10142903D077 +:102CA000152901D00C2010BD022180F88F110020C1 +:102CB00010BD2DE9FC410C464BF6803212219442A6 +:102CC0001DD8E4B16946FEF727FC002815D19DF810 +:102CD000000000F05FF9019E9DF80000703600F0E2 +:102CE00059F9019DAD1C2F88224639463046FDF723 +:102CF00065FC2888B842F6D10020BDE8FC81084672 +:102D0000FBE77CB5044600886946FEF705FC002811 +:102D100010D19DF8000000F03DF9019D9DF80000E4 +:102D2000703500F037F90198A27890F82C10914294 +:102D300001D10C207CBD7F212972A9720021E9728A +:102D4000E17880F82D10217980F82E10A17880F894 +:102D50002C1000207CBD1CB50C466946FEF7DCFB40 +:102D600000280AD19DF8000000F014F9019890F8AD +:102D70008C0000B10120207000201CBD7CB50D46E8 +:102D800014466946FEF7C8FB002809D19DF80000EB +:102D900000F000F9019890F82C00012801D00C20D7 +:102DA0007CBD9DF8000000F0F5F8019890F87810CF +:102DB000297090F87900207000207CBD70B50D4618 +:102DC0001646072101F072F818B381880124C388E0 +:102DD000428804EB4104AC4217D842F210746343BA +:102DE000A4106243B3FBF2F2521E94B24FF4FA7293 +:102DF000944200D91446A54200D22C46491C641CBA +:102E0000B4FBF1F24A43521E91B290F8C8211AB9AC +:102E100001E0022070BD01843180002070BD10B53A +:102E20000C46072101F042F840B1022C08D91220CB +:102E300010BD000018080020780000200220F7E7ED +:102E400014F0010180F8FD10C4F3400280F8FC206A +:102E500004D090F8FA1009B107F054FC0020E7E71D +:102E6000017889B1417879B141881B290CD38188D7 +:102E70001B2909D3C188022906D3F64902680A65CD +:102E800040684865002070471220704710B504461E +:102E90000EF086FD204607F0D8FB0020C8E710B5ED +:102EA00007F0D6FB0020C3E72DE9F04115460F4699 +:102EB00006460122114638460EF076FD04460121F1 +:102EC000384607F009FC844200D20446012130460E +:102ED00000F065F806460121002000F060F8311886 +:102EE000012096318C4206D901F19600611AB1FB9E +:102EF000F0F0401C80B228800020BDE8F08110B5C1 +:102F0000044600F077F808B10C2091E7601C0AF045 +:102F100038FE207800F00100FBF718FE207800F062 +:102F200001000CF010F8002082E710B504460720DD +:102F300000F056FF08B10C207AE72078C00711D0C6 +:102F400000226078114611F097FD08B112206FE75A +:102F5000A06809F01DFB6078D4F8041009F021FB8B +:102F6000002065E7002009F013FB00210846F5E783 +:102F700010B505F036FE00205AE710B5006805F0E0 +:102F800084F8002054E718B1022801D001207047CE +:102F90000020704708B1002070470120704710B52D +:102FA000012904D0022905D0FFDF204640E7C000F8 +:102FB000503001E080002C3084B2F6E710B50FF0FD +:102FC0009EF9042803D0052801D0002030E7012015 +:102FD0002EE710B5FFF7F2FF10B10CF07BF828B91F +:102FE00007F02EFD20B1FBF78CFD08B101201FE793 +:102FF00000201DE710B5FFF7E1FF18B907F020FD2D +:10300000002800D0012013E72DE9FE4300250F46DC +:1030100080460A260421404604F069FA4046FDF73E +:103020003EFE062000F0EAFE044616E06946062051 +:1030300000F0C5FE0BE000BFBDF80400B84206D0AA +:103040000298042241460E30FBF7CAF950B1684697 +:1030500000F093FE0500EFD0641E002C06DD002D6D +:10306000E4D005E04046FDF723FEF5E705B9FFDFB4 +:10307000D8F80000FDF737FE761E01D00028C9D031 +:10308000BDE8FE8390F8F01090F88C0020B919B1DB +:10309000042901D0012070470020704701780029E1 +:1030A0000AD0416891F8FA20002A05D0002281F860 +:1030B000FA20406807F026BB704770B514460546F5 +:1030C000012200F01BF9002806D121462846BDE860 +:1030D0007040002200F012B970BDFB2802D8B1F593 +:1030E000296F01D911207047002070471B38E12853 +:1030F00006D2B1F5A47F03D344F29020814201D9D6 +:1031000012207047002070471FB55249403191F896 +:103110002010CA0702D102781D2A0AD08A0702D4D9 +:1031200002781C2A28D049073DD40178152937D0C8 +:1031300039E08088ADF8000002A9FEF7EDF900B192 +:10314000FFDF9DF80800FFF725FF039810F8601FC8 +:103150008DF8021040788DF803000020ADF80400CF +:1031600001B9FFDF9DF8030000B9FFDF6846FEF7F5 +:1031700040FCD8B1FFDF19E08088ADF800004FF4C3 +:103180002961FB20ADF80410ADF80200ADF806008F +:10319000ADF808106846FEF73AFD38B1FFDF05E0EC +:1031A000807BC00702D0002004B041E60120FBE78D +:1031B000F8B50746508915460C4640B1B0F5004FAA +:1031C00005D20022A878114611F056FC08B1122051 +:1031D000F8BDA06E04F1700630B1A97894F86E00C5 +:1031E000814201D00C20F8BD012184F86F10A9782C +:1031F00084F86E106968A1666989A4F86C10288942 +:10320000B084002184F86F1028886946FEF762FFB9 +:10321000B08CBDF80010081A00B2002804DD214669 +:103220003846FEF745FFDDE70020F8BD042803D34C +:1032300021B9B0F5804F01D90020704701207047B7 +:10324000042803D321B9B0F5804F01D9002070477D +:1032500001207047D8070020012802D018B10020B3 +:103260007047022070470120704710B500224FF4CC +:10327000C84408E030F81230A34200D9234620F8B1 +:103280001230521CD2B28A42F4D3D1E580B2C106C8 +:103290000BD401071CD481064FEAC07101D5B9B91E +:1032A00000E099B1800713D410E0410610D48106E4 +:1032B0000ED4C1074FEA807104D0002902DB400719 +:1032C00004D405E0010703D4400701D4012070476E +:1032D0000020704770B50C460546FF2904D8FBF75F +:1032E0007CFA18B11F2C01D9122070BD2846FBF7BB +:1032F0005EFA08B1002070BD422070BD0AB1012203 +:1033000000E00222024202D1C80802D109B1002025 +:10331000704711207047000030B5058825F400443F +:1033200021448CB24FF4004194420AD2121B92B253 +:103330001B339A4201D2A94307E005F4004121431F +:1033400003E0A21A92B2A9431143018030BD0844A0 +:10335000083050434A31084480B2704770B51D466A +:1033600016460B46044629463046049AFFF7EFFFFF +:103370000646B34200D2FFDF282200212046FBF799 +:1033800086F84FF6FF70A082283EB0B26577608065 +:10339000B0F5004F00D9FFDF618805F13C008142A4 +:1033A00000D2FFDF60880835401B343880B22080AF +:1033B0001B2800D21B2020800020A07770BD8161D7 +:1033C000886170472DE9F05F0D46C188044600F121 +:1033D0002809008921F4004620F4004800F063FB2E +:1033E00010B10020BDE8F09F4FF0000A4FF0010B34 +:1033F000B0450CD9617FA8EB0600401A0838854219 +:1034000019DC09EB06000021058041801AE0608884 +:10341000617F801B471A083F0DD41B2F00DAFFDFA6 +:10342000BD4201DC294600E0B9B2681A0204120C60 +:1034300004D0424502DD84F817A0D2E709EB06006C +:103440000180428084F817B0CCE770B5044600F1E3 +:103450002802C088E37D20F400402BB1104402888C +:10346000438813448B4201D2002070BD00258A425C +:1034700002D30180458008E0891A0904090C4180C3 +:1034800003D0A01D00F01FFB08E0637F0088083315 +:10349000184481B26288A01DFFF73EFFE575012048 +:1034A00070BD70B5034600F12804C588808820F4FB +:1034B00000462644A84202D10020188270BD988997 +:1034C0003588A84206D3401B75882D1A2044ADB21A +:1034D000C01E05E02C1AA5B25C7F20443044401D7C +:1034E0000C88AC4200D90D809C8924B10024147052 +:1034F0000988198270BD0124F9E770B5044600F10E +:103500002801808820F400404518208A002825D012 +:10351000A189084480B2A08129886A881144814227 +:1035200000D2FFDF2888698800260844A1898842E4 +:1035300012D1A069807F2871698819B1201D00F01F +:10354000C2FA08E0637F28880833184481B2628891 +:10355000201DFFF7E1FEA6812682012070BD2DE926 +:10356000F041418987880026044600F12805B942C8 +:1035700019D004F10A0800BF21F400402844418812 +:1035800019B1404600F09FFA08E0637F00880833D5 +:10359000184481B262884046FFF7BEFE761C6189FE +:1035A000B6B2B942E8D13046BDE8F0812DE9F0412C +:1035B00004460B4627892830A68827F40041B4F832 +:1035C0000A8001440D46B74201D10020ECE70AB160 +:1035D000481D106023B1627F691D1846FAF72DFF60 +:1035E0002E88698804F1080021B18A1996B200F08A +:1035F0006AFA06E0637F62880833991989B2FFF797 +:103600008BFE474501D1208960813046CCE7818817 +:10361000C088814201D10120704700207047018994 +:103620008088814201D1012070470020704770B529 +:103630008588C38800F1280425F4004223F4004162 +:1036400014449D421AD08389058A5E1925886388AF +:10365000EC18A64214D313B18B4211D30EE0437F72 +:1036600008325C192244408892B2801A80B2233317 +:10367000984201D211B103E08A4201D1002070BD0D +:10368000012070BD2DE9F0478846C18804460089B5 +:1036900021F4004604F1280720F4004507EB060951 +:1036A00000F001FA002178BBB54204D9627FA81B63 +:1036B000801A002503E06088627F801B801A08382A +:1036C00023D4E28962B1B9F80020B9F802303BB1E5 +:1036D000E81A2177404518DBE0893844801A09E070 +:1036E000801A217740450ADB607FE1890830304449 +:1036F00039440844C01EA4F81280BDE8F08745454F +:1037000003DB01202077E7E7FFE761820020F4E791 +:103710002DE9F74F044600F12805C088884620F4BB +:10372000004A608A05EB0A0608B1404502D2002033 +:10373000BDE8FE8FE08978B13788B6F8029007EBD4 +:103740000901884200D0FFDF207F4FF0000B50EAD4 +:10375000090106D088B33BE00027A07FB94630714D +:10376000F2E7E18959B1607F2944083050440844A8 +:10377000B4F81F1020F8031D94F821108170E2891D +:1037800007EB080002EB0801E1813080A6F802B0E7 +:1037900002985F4650B1637F30880833184481B285 +:1037A0006288A01DFFF7B8FDE78121E0607FE18915 +:1037B00008305044294408442DE0FFE7E089B4F87C +:1037C0001F102844C01B20F8031D94F8211081709D +:1037D00009EB0800E28981B202EB0800E081378042 +:1037E00071800298A0B1A01D00F06DF9A4F80EB090 +:1037F000A07F401CA077A07D08B1E088A08284F85B +:1038000016B000BFA4F812B084F817B001208FE7FB +:10381000E0892844C01B30F8031DA4F81F108078ED +:1038200084F82100EEE710B5818800F1280321F427 +:1038300000442344848AC288A14212D0914210D00D +:10384000818971B9826972B11046FFF7E8FE50B9FB +:103850001089283220F400401044197900798842F8 +:1038600001D1002010BD184610BD00F12803407F93 +:1038700008300844C01E1060088808B9DB1E1360B9 +:1038800008884988084480B270472DE9F04100F16A +:103890002806407F1C4608309046431808884D880B +:1038A000069ADB1EA0B1C01C80B2904214D9801AC7 +:1038B000A04200DB204687B298183A464146FAF704 +:1038C0008FFD002816D1E01B84B2B844002005E02B +:1038D000ED1CADB2F61EE8E7101A80B20119A9423C +:1038E00006D8304422464146BDE8F041FAF778BD9B +:1038F0004FF0FF3058E62DE9F04100F12804407FF9 +:103900001E46083090464318002508884F88069ABE +:10391000DB1E90B1C01C80B2904212D9801AB04216 +:1039200000DB304685B299182A464046FAF785FDF5 +:10393000701B86B2A844002005E0FF1CBFB2E41E45 +:10394000EAE7101A80B28119B94206D82118324626 +:103950004046FAF772FDA81985B2284624E62DE9FB +:10396000F04100F12804407F1E460830904643187D +:10397000002508884F88069ADB1E90B1C01C80B2D3 +:10398000904212D9801AB04200DB304685B29818B6 +:103990002A464146FAF751FD701B86B2A844002022 +:1039A00005E0FF1CBFB2E41EEAE7101A80B28119DD +:1039B000B94206D8204432464146FAF73EFDA819DE +:1039C00085B22846F0E5401D704710B5044600F169 +:1039D0002801C288808820F400431944904206D010 +:1039E000A28922B9228A12B9A28A904201D100206A +:1039F00010BD0888498831B1201D00F064F800200E +:103A00002082012010BD637F62880833184481B290 +:103A1000201DFFF781FCF2E70021C181017741827F +:103A2000C1758175704703881380C28942B1C2880D +:103A300022F4004300F128021A440A60C08970474A +:103A40000020704710B50446808AA0F57F41FF39F9 +:103A500000D0FFDFE088A082E08900B10120A075DE +:103A600010BD4FF6FF71818200218175704710B53E +:103A70000446808AA0F57F41FF3900D1FFDFA07D99 +:103A800028B9A088A18A884201D1002010BD012058 +:103A900010BD8188828A914201D1807D08B10020C9 +:103AA00070470120704720F4004221F400439A42FD +:103AB00007D100F4004001F40041884201D0012008 +:103AC00070470020704730B5044600880D4620F44A +:103AD0000040A84200D2FFDF21884FF40040884315 +:103AE0002843208030BD70B50C00054609D0082C55 +:103AF00000D2FFDF1DB1A1B2286800F044F8201DFC +:103B000070BD0DB100202860002070BD002102684A +:103B100003E093881268194489B2002AF9D100F0B1 +:103B200032B870B500260D460446082900D2FFDFE2 +:103B3000206808B91EE0044620688188A94202D0A6 +:103B400001680029F7D181880646A94201D10068A1 +:103B50000DE005F1080293B20022994209D32844EE +:103B6000491B026081802168096821600160206032 +:103B700000E00026304670BD00230B608A8002689A +:103B80000A600160704700234360021D01810260EA +:103B90007047F0B50F460188408815460C181E4640 +:103BA000AC4200D3641B3044A84200D9FFDFA01907 +:103BB000A84200D9FFDF3819F0BD2DE9F041884651 +:103BC00006460188408815460C181F46AC4200D3B3 +:103BD000641B3844A84200D9FFDFE019A84200D98D +:103BE000FFDF70883844708008EB0400BDE8F08186 +:103BF0002DE9F041054600881E461746841B88467D +:103C0000BC4200D33C442C8068883044B84200D980 +:103C1000FFDFA019B84200D9FFDF68883044688010 +:103C200008EB0400E2E72DE9F04106881D46044652 +:103C3000701980B2174688462080B84201D3C01B55 +:103C400020806088A84200D2FFDF7019B84200D9F6 +:103C5000FFDF6088401B608008EB0600C6E730B5D8 +:103C60000D460188CC18944200D3A41A408898428B +:103C700000D8FFDF281930BD2DE9F041C84D0446BA +:103C80009046A8780E46A04200D8FFDF05EB8607D5 +:103C9000B86A50F8240000B1FFDFB868002816D0D9 +:103CA000304600F044F90146B868FFF73AFF0500D6 +:103CB0000CD0B86A082E40F8245000D3FFDFB94872 +:103CC0004246294650F82630204698472846BDE807 +:103CD000F0812DE9F8431E468C1991460F460546A2 +:103CE000FF2C00D9FFDFB14500D9FFDFE4B200951A +:103CF0004DB300208046E81C20F00300A84200D00D +:103D0000FFDF4946DFF89892684689F8001089F885 +:103D1000017089F8024089F8034089F8044089F865 +:103D2000054089F8066089F80770414600F008F9F7 +:103D3000002142460F464B460098C01C20F003006D +:103D4000009012B10EE00120D4E703EB8106B062CF +:103D5000002005E0D6F828C04CF82070401CC0B206 +:103D6000A042F7D30098491C00EB8400C9B2009030 +:103D70000829E1D3401BBDE8F88310B50446EDF7F0 +:103D80008EFA08B1102010BD2078854A618802EBB8 +:103D9000800092780EE0836A53F8213043B14A1CC8 +:103DA0006280A180806A50F82100A060002010BDD0 +:103DB000491C89B28A42EED86180052010BD70B5D9 +:103DC00005460C460846EDF76AFA08B1102070BDAA +:103DD000082D01D3072070BD25700020608070BDC4 +:103DE0000EB56946FFF7EBFF00B1FFDF6846FFF74E +:103DF000C4FF08B100200EBD01200EBD10B5044661 +:103E0000082800D3FFDF6648005D10BD3EB50546BB +:103E100000246946FFF7D3FF18B1FFDF01E0641CFF +:103E2000E4B26846FFF7A9FF0028F8D02846FFF75C +:103E3000E5FF001BC0B23EBD59498978814201D9D6 +:103E4000C0B27047FF2070472DE9F041544B06295E +:103E500003D007291CD19D7900E0002500244FF6EE +:103E6000FF7603EB810713F801C00AE06319D7F866 +:103E700028E09BB25EF823E0BEF1000F04D0641C82 +:103E8000A4B2A445F2D8334603801846B34201D108 +:103E900000201CE7BDE8F041EEE6A0F57F43FF3BC4 +:103EA00001D0082901D300207047E5E6A0F57F4244 +:103EB000FF3A0BD0082909D2394A9378834205D9B1 +:103EC00002EB8101896A51F8200070470020704799 +:103ED0002DE9F04104460D46A4F57F4143F202006E +:103EE000FF3902D0082D01D30720F0E62C494FF00E +:103EF00000088A78A242F8D901EB8506B26A52F826 +:103F00002470002FF1D027483946203050F8252062 +:103F100020469047B16A284641F8248000F007F80F +:103F200002463946B068FFF727FE0020CFE61D495C +:103F3000403131F810004FF6FC71C01C084070474A +:103F40002DE9F843164E8846054600242868C01C13 +:103F500020F0030028602046FFF7E9FF315D484369 +:103F6000B8F1000F01D0002200E02A68014600925B +:103F700032B100274FEA0D00FFF7B5FD1FB106E093 +:103F800001270020F8E706EB8401009A8A6029687F +:103F9000641C0844E4B22860082CD7D3EBE6000088 +:103FA0003C0800201862020070B50E461D461146FE +:103FB00000F0D3F804462946304600F0D7F82044F4 +:103FC000001D70BD2DE9F04190460D4604004FF0F4 +:103FD000000610D00027E01C20F00300A04200D013 +:103FE000FFDFE5B141460020FFF77DFD0C3000EB1F +:103FF000850617B113E00127EDE7614F04F10C00CE +:10400000AA003C602572606000EB85002060002102 +:104010006068FAF73CFA41463868FFF764FD3046BD +:10402000BDE8F0812DE9FF4F554C804681B02068F6 +:104030009A46934600B9FFDF2068027A424503D9C9 +:10404000416851F8280020B143F2020005B0BDE8F4 +:10405000F08F5146029800F080F886B258460E99CB +:1040600000F084F885B27019001D87B22068A1465F +:1040700039460068FFF755FD04001FD06780258092 +:104080002946201D0E9D07465A4601230095FFF73D +:1040900065F92088314638440123029ACDF800A002 +:1040A000FFF75CF92088C1193846FFF788F9D9F87D +:1040B00000004168002041F82840C7E70420C5E718 +:1040C00070B52F4C0546206800B9FFDF2068017AE3 +:1040D000A9420DD9426852F8251049B1002342F88F +:1040E00025304A880068FFF747FD2168087A06E016 +:1040F00043F2020070BD4A6852F820202AB9401EDF +:10410000C0B2F8D20868FFF701FD002070BD70B59D +:104110001B4E05460024306800B9FFDF3068017A85 +:10412000A94204D9406850F8250000B1041D20467A +:1041300070BD70B5124E05460024306800B9FFDF2F +:104140003068017AA94206D9406850F8251011B1AB +:1041500031F8040B4418204670BD10B50A46012101 +:10416000FFF7F5F8C01C20F0030010BD10B50A469B +:104170000121FFF7ECF8C01C20F0030010BD000087 +:104180008C00002070B50446C2F110052819FAF71A +:1041900054F915F0FF0109D0491ECAB28020A0547D +:1041A0002046BDE870400021FAF771B970BD30B506 +:1041B00005E05B1EDBB2CC5CD55C6C40C454002BCC +:1041C000F7D130BD10B5002409E00B78521E44EA47 +:1041D000430300F8013B11F8013BD2B2DC09002A8D +:1041E000F3D110BD2DE9F04389B01E46DDE9107909 +:1041F00090460D00044622D002460846F949FDF7D4 +:1042000044FE102221463846FFF7DCFFE07B000623 +:1042100006D5F44A3946102310320846FFF7C7FF87 +:10422000102239464846FFF7CDFFF87B000606D539 +:10423000EC4A4946102310320846FFF7B8FF102217 +:1042400000212046FAF723F90DE0103EB6B208EB44 +:104250000601102322466846FFF7A9FF224628469A +:104260006946FDF712FE102EEFD818D0F2B2414683 +:104270006846FFF787FF10234A46694604A8FFF700 +:1042800096FF1023224604A96846FFF790FF2246B6 +:1042900028466946FDF7F9FD09B0BDE8F083102313 +:1042A0003A464146EAE770B59CB01E4605461346BD +:1042B00020980C468DF80800202219460DF10900BF +:1042C000FAF7BBF8202221460DF12900FAF7B5F8DC +:1042D00017A913A8CDE90001412302AA31462846B7 +:1042E000FFF780FF1CB070BD2DE9FF4F9FB014AEEB +:1042F000DDE92D5410AFBB49CDE9007620232031F4 +:104300001AA8FFF76FFF4FF000088DF808804FF0F4 +:1043100001098DF8099054F8010FCDF80A00A08822 +:10432000ADF80E0014F8010C1022C0F340008DF817 +:10433000100055F8010FCDF81100A888ADF8150050 +:1043400015F8010C2C99C0F340008DF8170006A851 +:104350008246FAF772F80AA8834610222299FAF7E1 +:104360006CF8A0483523083802AA40688DF83C80D4 +:10437000CDE900760E901AA91F98FFF733FF8DF84C +:1043800008808DF809902068CDF80A00A088ADF863 +:104390000E0014F8010C1022C0F340008DF810003C +:1043A0002868CDF81100A888ADF8150015F8010CA3 +:1043B0002C99C0F340008DF817005046FAF73DF8ED +:1043C000584610222299FAF738F8864835230838DB +:1043D00002AA40688DF83C90CDE900760E901AA9AB +:1043E0002098FFF7FFFE23B0BDE8F08FF0B59BB03B +:1043F0000C460546DDE922101E461746DDE920324F +:10440000D0F801C0CDF808C0B0F805C0ADF80CC0B8 +:104410000078C0F340008DF80E00D1F80100CDF80F +:104420000F00B1F80500ADF8130008781946C0F385 +:1044300040008DF815001088ADF8160090788DF8C2 +:1044400018000DF119001022F9F7F7FF0DF12900FE +:1044500010223146F9F7F1FF0DF1390010223946EB +:10446000F9F7EBFF17A913A8CDE90001412302AA30 +:1044700021462846FFF7B6FE1BB0F0BDF0B5A3B04D +:1044800017460D4604461E46102202A82899F9F741 +:10449000D4FF06A820223946F9F7CFFF0EA8202224 +:1044A0002946F9F7CAFF1EA91AA8CDE90001502331 +:1044B00002AA314616A8FFF795FE1698206023B091 +:1044C000F0BDF0B589B00446DDE90E070D46397838 +:1044D000109EC1F340018DF8001031789446C1F36D +:1044E00040018DF801101968CDF802109988ADF8D7 +:1044F000061099798DF808100168CDF809108188A7 +:10450000ADF80D1080798DF80F0010236A466146D2 +:1045100004A8FFF74CFE2246284604A9FDF7B5FC87 +:10452000D6F801000090B6F80500ADF80400D7F801 +:104530000100CDF80600B7F80500ADF80A0000202C +:10454000039010236A46214604A8FFF730FE224656 +:10455000284604A9FDF799FC09B0F0BD1FB51C68F9 +:1045600000945B68019313680293526803920246B9 +:1045700008466946FDF789FC1FBD10B588B00446A2 +:104580001068049050680590002006900790084637 +:104590006A4604A9FDF779FCBDF80000208008B048 +:1045A00010BD1FB51288ADF800201A88ADF80220A2 +:1045B0000022019202920392024608466946FDF7E4 +:1045C00064FC1FBD7FB5074B14460546083B9A1C8B +:1045D0006846FFF7E6FF224669462846FFF7CDFF0B +:1045E0007FBD00007062020070B5044600780E4680 +:1045F000012813D0052802D0092813D10EE0A068A5 +:1046000061690578042003F059FA052D0AD0782352 +:1046100000220420616903F0A7F903E00420616926 +:1046200003F04CFA31462046BDE8704001F08AB8EC +:1046300010B500F12D03C2799C78411D144064F33C +:104640000102C271D2070DD04A795C7922404A71C9 +:104650000A791B791A400A718278C9788A4200D98E +:10466000817010BD00224A71F5E74178012900D020 +:104670000C21017070472DE9F04F93B04FF0000B03 +:104680000C690D468DF820B0097801260C201746DC +:104690004FF00D084FF0110A4FF008091B2975D291 +:1046A000DFE811F01B00C40207031F035E03710360 +:1046B000A303B803F9031A0462049504A204EF04E7 +:1046C0002D05370555056005F305360639066806DC +:1046D0008406FE062207EB06F00614B120781D289A +:1046E0002AD0D5F808805FEA08004FD001208DF865 +:1046F0002000686A02220D908DF824200A208DF88F +:104700002500A8690A90A8880028EED098F8001023 +:1047100091B10F2910D27DD2DFE801F07C1349DE80 +:10472000FCFBFAF9F8F738089CF6F50002282DD1C1 +:1047300024B120780C2801D00026F0E38DF8202049 +:10474000CBE10420696A03F0B9F9A8880728EED103 +:10475000204600F0F2FF022809D0204600F0EDFFCD +:10476000032807D9204600F0E8FF072802D20120DD +:10477000207004E0002CB8D020780128D7D198F818 +:104780000400C11F0A2902D30A2061E0C4E1A0701D +:10479000D8F80010E162B8F80410218698F80600F5 +:1047A00084F83200012028700320207044E007289C +:1047B000BDD1002C99D020780D28B8D198F80310DD +:1047C00094F82F20C1F3C000C2F3C002104201D000 +:1047D000062000E00720890707D198F8051001425C +:1047E000D2D198F806100142CED194F8312098F831 +:1047F000051020EA02021142C6D194F8322098F83E +:10480000061090430142BFD198F80400C11F0A2945 +:10481000BAD200E008E2617D81427CD8D8F800106D +:104820006160B8F80410218198F80600A072012098 +:1048300028700E20207003208DF82000686A0D90EB +:1048400004F12D000990601D0A900F300B9022E1B9 +:104850002875FCE3412891D1204600F06EFF042822 +:1048600002D1E078C00704D1204600F066FF0F288F +:1048700084D1A88CD5F80C8080B24FF0400BE6694B +:10488000FFF745FC324641465B464E46CDF8009068 +:10489000FFF731F80B208DF82000686A0D90E06971 +:1048A0000990002108A8FFF79FFE2078042806D071 +:1048B000A07D58B1012809D003280AD04AE3052079 +:1048C0002070032028708DF82060CEE184F800A0CD +:1048D00032E712202070EAE11128BCD1204600F016 +:1048E0002CFF042802D1E078C00719D0204600F040 +:1048F00024FF062805D1E078C00711D1A07D022849 +:104900000ED0204608E0CCE084E072E151E124E1E1 +:1049100003E1E9E019E0B0E100F00FFF11289AD1BE +:10492000102208F1010104F13C00F9F786FD6078DE +:1049300001286ED012202070E078C00760D0A07DE2 +:104940000028C8D00128C6D05AE0112890D12046AE +:1049500000F0F3FE082804D0204600F0EEFE1328F5 +:1049600086D104F16C00102208F101010646F9F726 +:1049700064FD207808280DD014202070E178C80745 +:104980000DD0A07D02280AD06278022A04D0032824 +:10499000A1D035E00920F0E708B1012837D1C807D8 +:1049A00013D0A07D02281DD000200090D4E906215C +:1049B00033460EA8FFF777FC10220EA904F13C0045 +:1049C000F9F70EFDC8B1042042E7D4E90912201D11 +:1049D0008DE8070004F12C0332460EA8616BFFF747 +:1049E00070FDE9E7606BC1F34401491E0068C840EF +:1049F00000F0010040F08000D7E72078092806D1B8 +:104A000085F800908DF8209036E32870EFE30920B8 +:104A1000FBE79EE1112899D1204600F08EFE0A287E +:104A200002D1E078C00704D1204600F086FE1528A8 +:104A30008CD104F13C00102208F101010646F9F77F +:104A4000FCFC20780A2816D016202070D4E9093200 +:104A5000606B611D8DE80F0004F15C0304F16C02D2 +:104A600047310EA8FFF7C2FC10220EA93046F9F715 +:104A7000B7FC18B1F9E20B20207073E22046FFF773 +:104A8000D7FDA078216AC0F110020B18002118464A +:104A9000F9F7FDFC26E3394608A8FFF7A5FD064611 +:104AA0003CE20228B7D1204600F047FE042804D398 +:104AB000204600F042FE082809D3204600F03DFEC3 +:104AC0000E2829D3204600F038FE122824D2A07DDB +:104AD0000228A0D10E208DF82000686A0D9098F869 +:104AE00001008DF82400F5E3022894D1204600F05F +:104AF00024FE002810D0204600F01FFE0128F9D027 +:104B0000204600F01AFE0C28F4D004208DF8240072 +:104B100098F801008DF8250060E21128FCD1002CE6 +:104B2000FAD020781728F7D16178606A022912D06C +:104B30005FF0000101EB4101182606EBC1011022D4 +:104B4000405808F10101F9F778FC0420696A00F087 +:104B5000E7FD2670F0E50121ECE70B28DCD1002C05 +:104B6000DAD020781828D7D16078616A02281CD062 +:104B70005FF0000000EB4002102000EBC20009587B +:104B8000B8F8010008806078616A02280FD0002020 +:104B900000EB4002142000EBC2000958404650F8D8 +:104BA000032F0A604068486039E00120E2E70120F5 +:104BB000EEE71128B0D1002CAED020781928ABD167 +:104BC0006178606A022912D05FF0000101EB4101B7 +:104BD0001C2202EBC1011022405808F10101F9F733 +:104BE0002CFC0420696A00F09BFD1A20B6E001212C +:104BF000ECE7082890D1002C8ED020781A288BD191 +:104C0000606A98F80120017862F347010170616AD7 +:104C1000D8F8022041F8012FB8F806008880042057 +:104C2000696A00F07DFD90E2072011E638780128DE +:104C300094D1182204F114007968F9F7FEFBE079A9 +:104C4000C10894F82F0001EAD001E07861F3000078 +:104C5000E070217D002974D12178032909D0C00793 +:104C600025D0032028708DF82090686A0D9041208F +:104C700008E3607DA178884201D90620E8E5022694 +:104C80002671E179204621F0E001E171617A21F09D +:104C9000F0016172A17A21F0F001A172FFF7C8FC66 +:104CA0002E708DF82090686A0D900720EAE20420AB +:104CB000ABE6387805289DD18DF82000686A0D9004 +:104CC000B8680A900720ADF824000A988DF830B033 +:104CD0006168016021898180A17A8171042020703E +:104CE000F8E23978052985D18DF82010696A0D918F +:104CF000391D09AE0EC986E80E004121ADF8241019 +:104D00008DF830B01070A88CD7F80C8080B2402697 +:104D1000A769FFF70EFA41463A463346C846CDF832 +:104D20000090FEF71CFE002108A8FFF75DFCE0786C +:104D300020F03E00801CE0702078052802D00F2073 +:104D40000CE04AE1A07D20B1012802D0032802D066 +:104D500002E10720BEE584F80080EDE42070EBE47A +:104D6000102104F15C0002F0C2FB606BB0BBA07DBF +:104D700018B1012801D00520FDE006202870F84870 +:104D80006063A063C2E23878022894D1387908B110 +:104D90002875B7E3A07D022802D0032805D022E0C1 +:104DA000B8680028F5D060631CE06078012806D060 +:104DB000A07994F82E10012805D0E94806E0A179E1 +:104DC00094F82E00F7E7B8680028E2D06063E07836 +:104DD000C00701D0012902D0E14803E003E0F868F0 +:104DE0000028D6D0A06306200FE68DF82090696ACF +:104DF0000D91E1784846C90709D06178022903D1AD +:104E0000A17D29B1012903D0A17D032900D007206C +:104E1000287033E138780528BBD1207807281ED0C8 +:104E200084F800A005208DF82000686A0D90B8680D +:104E30000A90ADF824A08DF830B003210170E1781C +:104E4000CA070FD0A27D022A1AD000210091D4E90E +:104E5000061204F15C03401CFFF725FA6BE384F8AB +:104E60000090DFE7D4E90923211D8DE80E0004F14D +:104E70002C0304F15C02401C616BFFF722FB5AE338 +:104E8000626BC1F34401491E1268CA4002F001017D +:104E900041F08001DAE738780528BDD18DF820008F +:104EA000686A0D90B8680A90ADF824A08DF830B00B +:104EB000042100F8011B102204F15C01F9F7BDFA8E +:104EC000002108A8FFF790FB2078092801D01320C3 +:104ED00044E70A2020709AE5E078C10742D0A17D1E +:104EE000012902D0022927D038E0617808A80129D9 +:104EF00016D004F16C010091D4E9061204F15C03B0 +:104F0000001DFFF7BBFA0A20287003268DF82080C9 +:104F1000686A0D90002108A8FFF766FBE1E2C7E28E +:104F200004F15C010091D4E9062104F16C03001D39 +:104F3000FFF7A4FA0026E9E7C0F3440114290DD2D3 +:104F40004FF0006101EBB0104FEAB060E0706078A4 +:104F5000012801D01020BDE40620FFE6607801287A +:104F60003FF4B6AC0A2050E5E178C90708D0A17D2E +:104F7000012903D10B202870042030E028702EE096 +:104F80000E2028706078616B012818D004F15C0352 +:104F900004F16C020EA8FFF7E1FA2046FFF748FB88 +:104FA000A0780EAEC0F1100230440021F9F76FFA7C +:104FB00006208DF82000686A09960D909BE004F1A8 +:104FC0006C0304F15C020EA8FFF7C8FAE8E7397831 +:104FD000022903D139790029D0D0297592E28DF8C0 +:104FE0002000686A0D9056E538780728F6D1D4E994 +:104FF00009216078012808D004F16C00CDE9000295 +:10500000029105D104F16C0304E004F15C00F5E7C2 +:1050100004F15C0304F14C007A680646216AFFF74C +:1050200063F96078012822D1A078216AC0F11002CA +:105030000B1800211846F9F72AFAD4E90923606B06 +:1050400004F12D018DE80F0004F15C0300E05BE248 +:1050500004F16C0231460EA8FFF7C8F910220EA920 +:1050600004F13C00F9F7BCF908B10B20ACE485F879 +:10507000008000BF8DF82090686A0D908DF824A004 +:1050800009E538780528A9D18DF82000686A0D90C7 +:10509000B8680A90ADF824A08DF830B080F8008090 +:1050A000617801291AD0D4E9092104F12D03A66BF6 +:1050B00003910096CDE9013204F16C0304F15C0226 +:1050C00004F14C01401CFFF791F9002108A8FFF7FB +:1050D0008BFA6078012805D015203FE6D4E9091243 +:1050E000631DE4E70E20287006208DF82000686A12 +:1050F000CDF824B00D90A0788DF82800CBE4387856 +:105100000328C0D1E079C00770D00F202870072095 +:1051100065E7387804286BD11422391D04F1140096 +:10512000F9F78BF9616A208CA1F80900616AA0780F +:10513000C871E179626A01F003011172616A627AF1 +:105140000A73616AA07A81F8240016205DE485F86C +:1051500000A08DF82090696A50460D9192E0000001 +:10516000706202003878052842D1B868A861617879 +:10517000606A022901D0012100E0002101EB410118 +:10518000142606EBC1014058082102F0B0F96178FD +:10519000606A022901D0012100E0002101EB4101F8 +:1051A00006EBC101425802A8E169FFF70BFA6078EB +:1051B000626A022801D0012000E0002000EB4001DB +:1051C000102000EBC1000223105802A90932FEF79B +:1051D000EEFF626AFD4B0EA80932A169FFF7E1F903 +:1051E0006178606A022904D0012103E044E18DE086 +:1051F000BFE0002101EB4101182606EBC101A278B6 +:1052000040580EA9F9F719F96178606A022901D0AE +:10521000012100E0002101EB410106EBC1014158F1 +:10522000A0780B18C0F1100200211846F9F72FF9E9 +:1052300005208DF82000686A0D90A8690A90ADF8E5 +:1052400024A08DF830B0062101706278616A022ACC +:1052500001D0012200E0002202EB420206EBC20272 +:10526000401C89581022F9F7E8F8002108A8FFF738 +:10527000BBF91220C5F818B028708DF82090686A24 +:105280000D900B208DF8240005E43878052870D1A6 +:105290008DF82000686A0D90B8680A900B20ADF870 +:1052A00024000A98072101706178626A022901D0FE +:1052B000012100E0002101EB4103102101EBC301BA +:1052C00051580988A0F801106178626A022902D059 +:1052D000012101E02FE1002101EB4103142101EB49 +:1052E000C30151580A6840F8032F4968416059E0EA +:1052F0001920287001208DF8300074E616202870DF +:105300008DF830B0002108A8FFF76EF9032617E1E9 +:1053100014202870AEE6387805282AD18DF82000B0 +:10532000686A0D90B8680A90ADF824A08DF830B086 +:1053300080F800906278616A4E46022A01D001220C +:1053400000E0002202EB42021C2303EBC202401CDD +:1053500089581022F9F771F8002108A8FFF744F9DD +:10536000152028708DF82060686A0D908DF82460F3 +:1053700039E680E0387805287DD18DF82000686A0C +:105380000D90B8680A90ADF8249009210170616908 +:10539000097849084170616951F8012FC0F802206D +:1053A0008988C18020781C28A8D1A1E7E078C007AF +:1053B00002D04FF0060C01E04FF0070C6078022895 +:1053C0000AD000BF4FF0000000EB040101F1090119 +:1053D00005D04FF0010004E04FF00100F4E74FF07A +:1053E00000000B78204413EA0C030B7010F8092F0F +:1053F00002EA0C02027004D14FF01B0C84F800C0CA +:10540000D2B394F801C0BCF1010F00D09BB990F861 +:1054100000C0E0465FEACC7C04D028F001060670AC +:10542000102606E05FEA887C05D528F002060670A3 +:1054300013262E70032694F801C0BCF1020F00D091 +:1054400092B991F800C05FEACC7804D02CF0010644 +:105450000E70172106E05FEA8C7805D52CF0020665 +:105460000E701921217000260078D0BBCAB3C3BBCF +:105470001C20207035E012E002E03878062841D187 +:105480001A2015E4207801283CD00C283AD0204678 +:10549000FFF7EBF809208DF82000686A0D9031E0E5 +:1054A0003878052805D00620387003261820287083 +:1054B00046E005208DF82000696A0D91B9680A91CF +:1054C0000221ADF8241001218DF830100A990870DE +:1054D000287D4870394608A8FFF786F80646182048 +:1054E0002870012E0ED02BE001208DF82000686A74 +:1054F0000D9003208DF82400287D8DF8250085F877 +:1055000014B012E0287D80B11D2020701720287073 +:105510008DF82090686A0D9002208DF8240039469D +:1055200008A8FFF761F806460AE00CB1FE202070DB +:105530009DF8200020B1002108A8FFF755F80CE4E1 +:1055400013B03046BDE8F08F2DE9F04387B00C462C +:105550004E6900218DF804100120257803460227AA +:105560004FF007094FF0050C85B1012D53D0022DE6 +:1055700039D1FE2030708DF80030606A059003202C +:105580008DF80400207E8DF8050063E02179012963 +:1055900025D002292DD0032928D0042923D1B17D7B +:1055A000022920D131780D1F042D04D30A3D032D8B +:1055B00001D31D2917D12189022914D38DF8047034 +:1055C000237020899DF80410884201E0686202007F +:1055D00018D208208DF80000606A059057E07078B6 +:1055E0000128EBD0052007B0BDE8F0831D20307006 +:1055F000E4E771780229F5D131780C29F3D18DF8DF +:105600000490DDE7083402F804CB94E80B0082E84C +:105610000B000320E7E71578052DE4D18DF800C0D5 +:10562000656A0595956802958DF8101094F80480C8 +:10563000B8F1010F13D0B8F1020F2DD0B8F1030F5C +:105640001CD0B8F1040FCED1ADF804700E20287034 +:10565000207E687000216846FEF7C6FF0CE0ADF8BA +:1056600004700B202870207E002100F01F0068705D +:105670006846FEF7B9FF37700020B4E7ADF8047054 +:105680008DF8103085F800C0207E687027701146B4 +:105690006846FEF7A9FFA6E7ADF804902B70207FBF +:1056A0006870607F00F00100A870A07F00F01F000C +:1056B000E870E27F2A71C0071CD094F8200000F047 +:1056C0000700687194F8210000F00700A87100211C +:1056D0006846FEF789FF2868F062A8883086A879B6 +:1056E00086F83200A069407870752879B0700D2076 +:1056F0003070C1E7A9716971E9E700B587B0042886 +:105700000CD101208DF800008DF8040000200591D7 +:105710008DF8050001466846FEF766FF07B000BD3C +:1057200070B50C46054602F0C9F921462846BDE889 +:1057300070407823002202F017B908B10078704752 +:105740000C20704770B50C0005784FF000010CD0AC +:1057500021702146EFF7D1FD69482178405D8842EC +:1057600001D1032070BD022070BDEFF7C6FD0020FF +:1057700070BD0279012A05D000220A704B78012BF6 +:1057800002D003E0042070470A758A610279930011 +:10579000521C0271C15003207047F0B587B00F460C +:1057A00005460124287905EB800050F8046C7078D8 +:1057B000411E02290AD252493A46083901EB8000BB +:1057C000314650F8043C2846984704460CB1012C59 +:1057D00011D12879401E10F0FF00287101D0032458 +:1057E000E0E70A208DF80000706A0590002101961C +:1057F0006846FFF7A7FF032CD4D007B02046F0BDC2 +:1058000070B515460A46044629461046FFF7C5FFFF +:10581000064674B12078FE280BD1207C30B10020E0 +:105820002870294604F10C00FFF7B7FF2046FEF769 +:105830001CFF304670BD704770B50E4604467C2292 +:105840000021F8F724FE0225012E03D0022E04D0F9 +:10585000052070BD0120607000E065702046FEF7F5 +:1058600004FFA575002070BD28B1027C1AB10A465C +:1058700000F10C01C4E70120704710B5044686B062 +:10588000042002F01BF92078FE2806D000208DF8B5 +:10589000000069462046FFF7E7FF06B010BD7CB563 +:1058A0000E4600218DF804104178012903D0022909 +:1058B00003D0002405E0046900E044690CB1217CB8 +:1058C00089B16D4601462846FFF753FF032809D1E9 +:1058D000324629462046FFF793FF9DF80410002921 +:1058E00000D004207CBD04F10C05EBE730B40C467D +:1058F0000146034A204630BC024B0C3AFEF751BE2B +:10590000AC6202006862020070B50D46040011D05E +:1059100085B1220100212846F8F7B9FD102250492F +:105920002846F8F78AFD4F48012101704470456010 +:10593000002070BD012070BD70B505460024494EA1 +:1059400011E07068AA7B00EB0410817B914208D1C2 +:10595000C17BEA7B914204D10C222946F8F740FD35 +:1059600030B1641CE4B230788442EAD3002070BDC8 +:10597000641CE0B270BD70B50546FFF7DDFF00287E +:1059800005D1384C20786178884201D3002070BD61 +:105990006168102201EB00102946F8F74EFD2078CF +:1059A000401CC0B2207070BD2E48007870472D4951 +:1059B0000878012802D0401E08700020704770B59A +:1059C0000D460021917014461180022802D0102843 +:1059D00015D105E0288890B10121A17010800CE05C +:1059E000284613B1FFF7C7FF01E0FFF7A5FFA0703E +:1059F00010F0FF0F03D0A8892080002070BD012087 +:105A000070BD0023DBE770B5054614460E0009D0D3 +:105A100000203070A878012806D003D911490A78EF +:105A200090420AD9012070BD24B1287820702888BE +:105A3000000A5070022008700FE064B1496810221B +:105A400001EB001120461039F8F7F7FC2878207395 +:105A50002888000A607310203070002070BD00009C +:105A6000BB620200900000202DE9F04190460C46F8 +:105A700007460025FE48072F00EB881607D2DFE80F +:105A800007F00707070704040400012500E0FFDF13 +:105A900006F81470002D13D0F548803000EB880113 +:105AA00091F82700202803D006EB4000447001E065 +:105AB00081F8264006EB44022020507081F82740F0 +:105AC000BDE8F081F0B51F4614460E460546202A73 +:105AD00000D1FFDFE649E648803100EB871C0CEB84 +:105AE000440001EB8702202E07D00CEB46014078E2 +:105AF0004B784870184620210AE092F8253040780B +:105B000082F82500F6E701460CEB4100057040786D +:105B1000A142F8D192F82740202C03D00CEB44048A +:105B2000637001E082F826300CEB4104202363709F +:105B300082F82710F0BD30B50D46CE4B4419002237 +:105B4000181A72EB020100D2FFDFCB48854200DD5C +:105B5000FFDFC9484042854200DAFFDFC548401CEC +:105B6000844207DA002C01DB204630BDC148401CCE +:105B7000201830BDBF48C043FAE710B5044601689D +:105B8000407ABE4A52F82020114450B10220084405 +:105B900020F07F40EDF763F894F90810BDE810405D +:105BA000C9E70420F3E72DE9F047B14E803696F8B7 +:105BB0002D50DFF8BC9206EB850090F8264034E0CB +:105BC00009EB85174FF0070817F81400012806D0D5 +:105BD00004282ED005282ED0062800D0FFDF01F0A3 +:105BE00025F9014607EB4400427806EB850080F872 +:105BF000262090F82720A24202D1202280F82720D8 +:105C0000084601F01EF92A4621460120FFF72CFF25 +:105C10009B48414600EB041002682046904796F8E6 +:105C20002D5006EB850090F82640202CC8D1BDE809 +:105C3000F087022000E003208046D0E710B58C4CAE +:105C40002021803484F8251084F8261084F8271049 +:105C5000002084F8280084F82D0084F82E10411EBE +:105C6000A16044F8100B2074607420736073A073FB +:105C70008449E07720750870487000217C4A103C08 +:105C800002F81100491CC9B22029F9D30120ECF710 +:105C9000D6FE0020ECF7D3FE012084F82200EDF7B9 +:105CA000FFF87948EDF711F9764CA41E207077487B +:105CB000EDF70BF96070BDE81040ECF74DBE10B584 +:105CC000ECF76FFE6F4CA41E2078EDF717F96078A3 +:105CD000EDF714F9BDE8104001F0E0B8202070475E +:105CE0000020ECF785BE70B5054601240E46AC4099 +:105CF0005AB1FFF7F5FF0146654800EBC500C0F853 +:105D00001015C0F81465634801E06248001D046086 +:105D100070BD2DE9F34F564C0025803404EB810A09 +:105D200089B09AF82500202821D0691E0291544993 +:105D3000009501EB0017391D03AB07C983E8070085 +:105D4000A18BADF81C10A07F8DF81E009DF81500EA +:105D5000A046C8B10226494951F820400399A2192A +:105D6000114421F07F41019184B102210FE0012013 +:105D7000ECF765FE0020ECF762FEECF730FE01F078 +:105D80008DF884F82F50A9E00426E4E700218DF86F +:105D90001810022801D0012820D103980119099870 +:105DA000081A801C9DF81C1020F07F4001B10221D0 +:105DB000353181420BD203208DF815000398C4F1D0 +:105DC0003201401A20F07F40322403900CE098F812 +:105DD000240018B901F043FA002863D0322C03D212 +:105DE00014B101F04FF801E001F058F8254A10789D +:105DF00018B393465278039B121B00219DF818405C +:105E0000994601281AD0032818D000208DF81E00CA +:105E1000002A04DD981A039001208DF818009DF8DF +:105E20001C0000B1022103981B4A20F07F40039020 +:105E300003AB099801F03EF810B110E00120E5E74E +:105E40009DF81D0018B99BF80000032829D08DF893 +:105E50001C50CDF80C908DF818408DF81E509DF810 +:105E6000180010B30398012381190022184615E089 +:105E7000840A0020FF7F841E0020A107CC6202005C +:105E8000840800209A00002017780100A75B010019 +:105E900000F0014004F50140FFFF3F00ECF722FE57 +:105EA00006E000200BB0BDE8F08F0120ECF7C7FD45 +:105EB00097F90C20012300200199ECF713FEF87BE1 +:105EC000C00701D0ECF7F7FE012188F82F108AF8FF +:105ED000285020226946FE48F8F7AFFA0120E1E792 +:105EE0002DE9F05FDFF8E883064608EB860090F8BE +:105EF0002550202D1FD0A8F180002C4600EB8617DE +:105F0000A0F50079DFF8CCB305E0A24607EB4A0024 +:105F10004478202C0AD0ECF730FE09EB04135A46E3 +:105F200001211B1D00F0C6FF0028EED0AC4202D0BC +:105F3000334652461EE0E84808B1AFF30080ECF764 +:105F40001CFE98F82F206AB1D8F80C20411C891A41 +:105F50000902CA1701EB12610912002902DD0020B3 +:105F6000BDE8F09F3146FFF7D4FE08B10120F7E706 +:105F700033462A4620210420FFF7A4FDEFE72DE950 +:105F8000F041D34C2569ECF7F8FD401B0002C11726 +:105F900000EB1160001200D4FFDF94F8220000B182 +:105FA000FFDF012784F8227094F82E00202800D10A +:105FB000FFDF94F82E60202084F82E00002584F85E +:105FC0002F5084F8205084F82150C4482560007870 +:105FD000022833D0032831D000202077A068401C4D +:105FE00005D04FF0FF30A0600120ECF728FD002025 +:105FF000ECF725FDECF721FEECF719FEECF7EFFCD2 +:106000000EF0D6FDB648056005604FF0E0214FF474 +:106010000040B846C1F88002ECF7BBFE94F82D7042 +:106020003846FFF75DFF0028FAD0A948803800EB1A +:10603000871010F81600022802D006E00120CCE7F5 +:106040003A4631460620FFF70FFD84F8238004EB23 +:10605000870090F82600202804D0A048801E4078B1 +:10606000ECF752FF207F002803D0ECF7D6FD257710 +:10607000657725E5964910B591F82D2000248039E3 +:1060800001EB821111F814302BB1641CE4B2202C06 +:10609000F8D3202010BD934901EB041108600020C3 +:1060A000C87321460120FFF7DFFC204610BD10B564 +:1060B000012801D0032800D171B3854A92F82D3010 +:1060C000834C0022803C04EB831300BF13F8124082 +:1060D0000CB1082010BD521CD2B2202AF6D37F4A40 +:1060E00048B1022807D0072916D2DFE801F01506CB +:1060F000080A0C0E100000210AE01B2108E03A21DA +:1061000006E0582104E0772102E0962100E0B52165 +:1061100051701070002010BD072010BD6F4810B5E1 +:106120004078ECF79CFD80B210BD10B5202811D24C +:10613000674991F82D30A1F1800202EB831414F825 +:1061400010303BB191F82D3002EB831212F8102081 +:10615000012A01D0002010BD91F82D200146002019 +:10616000FFF782FC012010BD10B5ECF706FDBDE87D +:106170001040ECF774BD2DE9F0410E46544F017804 +:106180002025803F0C4607EB831303E0254603EBF5 +:1061900045046478944202D0202CF7D108E0202CEA +:1061A00006D0A14206D103EB41014978017007E016 +:1061B000002085E403EB440003EB45014078487080 +:1061C000494F7EB127B1002140F22D40AFF300804E +:1061D0003078A04206D127B100214FF48660AFF39A +:1061E0000080357027B1002140F23540AFF30080C8 +:1061F000012065E410B542680B689A1A1202D417A0 +:1062000002EB1462121216D4497A91B1427A82B921 +:10621000364A006852F82110126819441044001DD3 +:10622000891C081A0002C11700EB11600012322805 +:1062300001DB012010BD002010BD2DE9F047294EE3 +:10624000814606F500709846144600EB811712E06F +:1062500006EB0415291D4846FFF7CCFF68B988F8FE +:106260000040A97B99F80A00814201D80020DEE4B1 +:1062700007EB44004478202CEAD10120D7E42DE933 +:10628000F047824612480E4600EB8600DFF8548045 +:1062900090F825402020107008F5007099461546AA +:1062A00000EB86170BE000BF08EB04105146001D01 +:1062B000FFF7A0FF28B107EB44002C704478202C96 +:1062C000F2D1297889F800104B46224631460FE07A +:1062D000040B0020FFFF3F00000000009A00002098 +:1062E00000F500408408002000000000CC6202009D +:1062F0005046BDE8F047A0E72DE9FC410F460446B3 +:106300000025FE4E10E000BF9DF80000256806EB5A +:1063100000108168204600F0E1FD2068A84202D10B +:106320000020BDE8FC8101256B4601AA39462046C4 +:10633000FFF7A5FF0028E7D02846F2E770B504462E +:10634000EF480125A54300EB841100EB85104022A6 +:10635000F8F773F8EB4E26B1002140F29D40AFF301 +:106360000080E748803000EB850100EB8400D0F826 +:106370002500C1F8250026B1002140F2A140AFF36D +:106380000080284670BD8A4203D003460520FFF7EF +:1063900099BB202906D0DA4A02EB801000EB4100BD +:1063A00040787047D649803101EB800090F8250095 +:1063B0007047D24901EB0010001DFFF7DEBB7CB532 +:1063C0001D46134604460E4600F1080221461846B3 +:1063D000ECF752FC94F908000F2804DD1F382072F6 +:1063E0002068401C206096B10220C74951F8261051 +:1063F000461820686946801B20F07F40206094F991 +:1064000008002844C01C1F2803DA012009E00420EA +:10641000EBE701AAECF730FC9DF8040010B10098FE +:10642000401C00900099206831440844C01C20F0B2 +:106430007F4060607CBDFEB50C46064609786079F9 +:10644000907220791F461546507279B12179002249 +:106450002846A368FFF7B3FFA9492846803191F881 +:106460002E20202A0AD00969491D0DE0D4E9022313 +:10647000217903B02846BDE8F040A0E7A349497858 +:10648000052900D20521314421F07F4100F026FD8D +:1064900039462846FFF730FFD4E9023221796846B1 +:1064A000FFF78DFF2B4600213046019A00F002FDD8 +:1064B000002806D103B031462846BDE8F04000F080 +:1064C0000DBDFEBD2DE9F14F84B000F0C3FCF0B16D +:1064D00000270498007800284FF000006DD1884D07 +:1064E000884C82468346803524B1002140F2045016 +:1064F000AFF3008095F82D8085F823B0002624B1F5 +:10650000002140F20950AFF3008017B105E00127E8 +:10651000DFE74046FFF712FF804624B1002140F23A +:106520001150AFF30080ECF728FB814643466A46E2 +:106530000499FFF780FF24B1002140F21750AFF318 +:10654000008095F82E0020280CD029690098401A68 +:106550000002C21700EB1260001203D5684600F07B +:10656000BDFC01264CB1002140F22150AFF3008068 +:10657000002140F22650AFF300806B46644A0021B0 +:10658000484600F097FC98B127B941466846FFF7A6 +:10659000B3FE064326B16846FFF7EFFA0499886018 +:1065A0004FF0010A24B1002140F23A50AFF30080CD +:1065B00095F82300002897D1504605B073E42DE9E3 +:1065C000F04F89B08B46824600F044FC4C4C80343E +:1065D00030B39BF80000002710B1012800D0FFDF86 +:1065E000484D25B1002140F2F950AFF300804349F6 +:1065F000012001EB0A18A946CDF81C005FEA090644 +:1066000004D0002140F20160AFF30080079800F051 +:1066100018FC94F82D50002084F8230067B119E08D +:1066200094F82E000127202800D1FFDF9BF80000FE +:106630000028D5D0FFDFD3E72846FFF77FFE0546C9 +:1066400026B1002140F20B60AFF3008094F82300E4 +:106650000028D3D126B1002140F21560AFF30080AD +:10666000ECF78BFA2B4602AA59460790FFF7E3FE98 +:1066700098F80F005FEA060900F001008DF813009A +:1066800004D0002140F21F60AFF300803B462A4651 +:1066900002A9CDF800A0079800F02BFC064604EBF9 +:1066A000850090F828000090B9F1000F04D0002177 +:1066B00040F22660AFF3008000F0B8FB0790B9F11C +:1066C000000F04D0002140F22C60AFF3008094F85A +:1066D0002300002892D1B9F1000F04D0002140F22C +:1066E0003460AFF300800DF1080C9CE80E00C8E99F +:1066F0000112C8F80C30BEB30CE000008408002082 +:10670000840A002000000000CC6202009A000020F1 +:10671000FFFF3F005FEA090604D0002140F241601C +:10672000AFF300800098B84312D094F82E002028D0 +:106730000ED126B1002140F24660AFF3008028461A +:10674000FFF7CEFB20B99BF80000D8B3012849D051 +:10675000B9F1000F04D0002140F26360AFF3008074 +:10676000284600F05CFB01265FEA090504D0002101 +:1067700040F26C60AFF30080079800F062FB25B137 +:1067800000214FF4CE60AFF300808EB194F82D005D +:1067900004EB800090F82600202809D025B10021C4 +:1067A00040F27760AFF30080F7484078ECF7ACFB3D +:1067B00025B1002140F27C60AFF3008009B0304683 +:1067C000BDE8F08FFFE7B9F1000F04D0002140F2DF +:1067D0004E60AFF3008094F82D2051460420FFF75F +:1067E00043F9C0E7002E3FF409AF002140F25960A1 +:1067F000AFF3008002E72DE9F84FE44D814695F8AC +:106800002D004FF00008E24C4FF0010B474624B139 +:10681000002140F28A60AFF30080584600F011FB7F +:1068200085F8237024B1002140F28F60AFF300801F +:1068300095F82D00FFF782FD064695F8230028B154 +:10684000002CE4D0002140F295604BE024B10021FF +:1068500040F29960AFF30080CC48803800EB86119D +:1068600011F81900032856D1334605EB830A4A462E +:106870009AF82500904201D1012000E0002000900C +:106880000AF125000021FFF776FC0146009801423D +:1068900003D001228AF82820AF77E1B324B1002188 +:1068A00040F29E60AFF30080324649460120FFF778 +:1068B000DBF89AF828A024B1002140F2A960AFF3D8 +:1068C000008000F0B3FA834624B1002140F2AE60AC +:1068D000AFF3008095F8230038B1002C97D0002149 +:1068E00040F2B260AFF3008091E7BAF1000F07D039 +:1068F00095F82E00202803D13046FFF7F1FAE0B1D9 +:1069000024B1002140F2C660AFF30080304600F0B1 +:1069100086FA4FF0010824B1002140F2CF60AFF3B6 +:106920000080584600F08DFA24B1002140F2D36077 +:10693000AFF300804046BDE8F88F002CF1D0002175 +:1069400040F2C160AFF30080E6E70120ECF750B8F9 +:106950008D48007870472DE9F0418C4C94F82E005A +:1069600020281FD194F82D6004EB860797F8255056 +:10697000202D00D1FFDF8549803901EB861000EB27 +:106980004500407807F8250F0120F87084F82300AF +:10699000294684F82E50324602202234FFF764F84C +:1069A0000020207005E42DE9F0417A4E774C012556 +:1069B00038B1012821D0022879D003287DD0FFDF0B +:1069C00017E400F05FFAFFF7C6FF207E00B1FFDF9B +:1069D00084F821500020ECF732F8A168481C04D05C +:1069E000012300221846ECF77DF814F82E0F2178C9 +:1069F00006EB01110A68012154E0FFF7ACFF01200A +:106A0000ECF71DF894F8210050B1A068401C07D0A5 +:106A100014F82E0F217806EB01110A68062141E0D7 +:106A2000207EDFF86481002708F10208012803D0E6 +:106A300002281ED0FFDFB5E7A777ECF7EEF898F84D +:106A40000000032801D165772577607D524951F810 +:106A5000200094F8201051B948B161680123091A47 +:106A600000221846ECF73EF8022020769AE72776B7 +:106A700098E784F8205000F005FAA07F50B198F80C +:106A8000010061680123091A00221846ECF72AF870 +:106A9000257600E0277614F82E0F217806EB0111F9 +:106AA0000A680021BDE8F041104700E005E03648E3 +:106AB0000078BDE8F041ECF727BAFFF74CFF14F877 +:106AC0002E0F217806EB01110A680521EAE710B5BF +:106AD0002E4C94F82E00202800D1FFDF14F82E0F42 +:106AE00021782C4A02EB01110A68BDE8104004210C +:106AF00010477CB5254C054694F82E00202800D17F +:106B0000FFDFA068401C00D0FFDF94F82E00214971 +:106B100001AA01EB0010694690F90C002844ECF73B +:106B2000ABF89DF904000F2801DD012000E00020F2 +:106B3000009908446168084420F07F41A16094F8FE +:106B40002100002807D002B00123BDE870400022D8 +:106B50001846EBF7C7BF7CBD30B5104A0B1A541C62 +:106B6000B3EB940F1ED3451AB5EB940F1AD393428F +:106B700003D9101A43185B1C14E0954210D9511A1E +:106B80000844401C43420DE098000020040B002004 +:106B90000000000084080020CC620200FF7F841EF9 +:106BA000FFDF0023184630BD0123002201460220EA +:106BB000EBF798BF0220EBF742BFEBF7DEBF2DE902 +:106BC000FE4FEE4C05468A4694F82E00202800D150 +:106BD000FFDFEA4E94F82E10A0462046A6F520725C +:106BE00002EB011420218DF8001090F82D10376968 +:106BF00000EB8101D8F8000091F82590284402AA02 +:106C000001A90C36ECF738F89DF90800002802DDE0 +:106C10000198401C0190A0680199642D084452D34A +:106C2000D74B00225B1B72EB02014CD36168411A07 +:106C300021F07F41B1F5800F45D220F07F40706098 +:106C400086F80AA098F82D1044466B464A4630460E +:106C5000FFF7F3FAB0B3A068401C10D0EBF78DFF3C +:106C6000A168081A0002C11700EB11600012022887 +:106C70002BDD0120EBF7E3FE4FF0FF30A06094F82E +:106C80002D009DF8002020210F34FFF77CFBA17F11 +:106C9000BA4A803A02EB8111E27F01EB420148706F +:106CA00054F80F0C284444F80F0C012020759DF86F +:106CB0000000202803D0B3484078ECF725F90120E4 +:106CC000BDE8FE8F01E00020FAE77760FBE72DE9E1 +:106CD000F047AA4C074694F82D00A4F1800606EB75 +:106CE000801010F8170000B9FFDF94F82D50A0466F +:106CF000A54C24B1002140F6EA00AFF3008040F635 +:106D0000F60940F6FF0A06EB851600BF16F81700D5 +:106D1000012819D0042811D005280FD006280DD03D +:106D20001CB100214846AFF300800FF02DF8002C75 +:106D3000ECD000215046AFF30080E7E72A46394601 +:106D40000120FEF791FEF2E74FF0010A4FF0000933 +:106D5000454624B1002140F60610AFF300805046AE +:106D600000F06FF885F8239024B1002140F60B1055 +:106D7000AFF3008095F82D00FFF7E0FA064695F88E +:106D8000230028B1002CE4D0002140F611101FE0B0 +:106D900024B1002140F61510AFF3008005EB86000A +:106DA00000F1270133463A462630FFF7E4F924B1D3 +:106DB000002140F61910AFF3008000F037F882464A +:106DC00095F8230038B1002CC3D0002140F61F10E5 +:106DD000AFF30080BDE785F82D60012085F8230022 +:106DE000504600F02EF8002C04D0002140F62C1064 +:106DF000AFF30080BDE8F08730B504465F480D462C +:106E000090F82D005D49803901EB801010F81400D6 +:106E100000B9FFDF5D4800EB0410C57330BD574972 +:106E200081F82D00012081F82300704710B55848E3 +:106E300008B1AFF30080EFF3108000F0010072B6EC +:106E400010BD10B5002804D1524808B1AFF300803E +:106E500062B610BD50480068C005C00D10D0103893 +:106E600040B2002804DB00F1E02090F8000405E0C7 +:106E700000F00F0000F1E02090F8140D4009704779 +:106E80000820704710B53D4C94F82400002804D128 +:106E9000F4F712FF012084F8240010BD10B5374C20 +:106EA00094F82400002804D0F4F72FFF002084F881 +:106EB000240010BD10B51C685B68241A181A24F051 +:106EC0007F4420F07F40A14206D8B4F5800F03D262 +:106ED000904201D8012010BD002010BDD0E9003241 +:106EE000D21A21F07F43114421F07F41C0E90031E3 +:106EF00070472DE9FC418446204815468038089C9F +:106F000000EB85160F4616F81400012804D002285D +:106F100002D00020BDE8FC810B46204A01216046DA +:106F2000FFF7C8FFF0B101AB6A4629463846FFF7C4 +:106F3000A6F9B8B19DF804209DF800102846FFF787 +:106F400022FA06EB440148709DF8000020280DD07D +:106F500006EB400044702A4621460320FEF784FDDC +:106F60000120D7E72A4621460420F7E703480121FC +:106F700000EB850000F8254FC170ECE7040B002002 +:106F8000FF1FA107980000200000000084080020D7 +:106F9000000000000000000004ED00E0FFFF3F00E3 +:106FA0002DE9F041044680074FF000054FF001063F +:106FB0000CD56B480560066000F0E8F920B169481F +:106FC000016841F48061016024F00204E0044FF0A4 +:106FD000FF3705D564484660C0F8087324F4805430 +:106FE000600003D56148056024F08044E0050FD5BA +:106FF0005F48C0F80052C0F808735E490D60091D73 +:107000000D605C4A04210C321160066124F4807426 +:10701000A00409D558484660C0F80052C0F808736B +:107020005648056024F40054C4F38030C4F3C031E2 +:10703000884200D0FFDF14F4404F14D0504846601F +:10704000C0F808734F488660C0F80052C0F8087353 +:107050004D490D600A1D16608660C0F808730D600A +:10706000166024F4404420050AD5484846608660EE +:10707000C0F80873C0F848734548056024F40064FC +:107080000DF070FD4348044200D0FFDFBDE8F08101 +:10709000F0B50022202501234FEA020420FA02F174 +:1070A000C9072DD051B2002910DB00BF4FEA51179C +:1070B0004FEA870701F01F0607F1E02703FA06F6FB +:1070C000C7F88061BFF34F8FBFF36F8F0CDB00BF3A +:1070D0004FEA51174FEA870701F01F0607F1E02733 +:1070E00003FA06F6C7F8806204DB01F1E02181F8BB +:1070F000004405E001F00F0101F1E02181F8144D99 +:1071000002F10102AA42C9D3F0BD10B5224C2060A1 +:107110000846F4F7EAFE2068FFF742FF2068FFF711 +:10712000B7FF0DF045F900F092F90DF01BFD0DF0E1 +:1071300058FCEBF7B5FEBDE810400DF0EDB910B509 +:10714000154C2068FFF72CFF2068FFF7A1FF0DF01A +:1071500009FDF4F7C9FF0020206010BD0A20704728 +:10716000FC1F00403C17004000C0004004E5014007 +:10717000008000400485004000D0004004D500405D +:1071800000E0004000F0004000F5004000B000408A +:1071900008B50040FEFF0FFD9C00002070B5264999 +:1071A0000A680AB30022154601244B685B1C4B6039 +:1071B0000C2B00D34D600E7904FA06F30E681E42C4 +:1071C0000FD0EFF3108212F0010272B600D001224C +:1071D0000C689C430C6002B962B6496801600020EB +:1071E00070BD521C0C2AE0D3052070BD4FF0E02189 +:1071F0004FF48000C1F800027047EFF3108111F0E6 +:10720000010F72B64FF0010202FA00F20A48036859 +:1072100042EA0302026000D162B6E7E706480021B5 +:1072200001604160704701218140034800680840C7 +:1072300000D0012070470000A0000020012081073D +:10724000086070470121880741600021C0F80011E3 +:1072500018480170704717490120087070474FF0B7 +:107260008040D0F80001012803D01248007800289F +:1072700000D00120704710480068C00700D00120EE +:1072800070470D480C300068C00700D001207047DF +:107290000948143000687047074910310A68D20362 +:1072A00006D5096801F00301814201D10120704730 +:1072B00000207047A8000020080400404FF08050D4 +:1072C000D0F830010A2801D0002070470120704713 +:1072D00000B5FFF7F3FF20B14FF08050D0F8340134 +:1072E00008B1002000BD012000BD4FF08050D0F853 +:1072F00030010E2801D000207047012070474FF068 +:107300008050D0F83001062803D0401C01D0002066 +:107310007047012070474FF08050D0F830010D28A1 +:1073200001D000207047012070474FF08050D0F806 +:107330003001082801D000207047012070474FF02D +:107340008050D0F83001102801D000207047012073 +:10735000704700B5FFF7F3FF30B9FFF7DCFF18B94E +:10736000FFF7E3FF002800D0012000BD00B5FFF7C4 +:10737000C6FF38B14FF08050D0F83401062803D34F +:10738000401C01D0002000BD012000BD00B5FFF76A +:10739000B6FF48B14FF08050D0F83401062803D32F +:1073A000401C01D0012000BD002000BD0021017063 +:1073B000084670470146002008707047EFF31081BF +:1073C00001F0010172B60278012A01D0012200E029 +:1073D00000220123037001B962B60AB10020704790 +:1073E0004FF400507047E9E7EFF3108111F0010FFF +:1073F00072B64FF00002027000D162B600207047F2 +:10740000F2E700002DE9F04115460E46044600273C +:1074100000F0EBF8A84215D3002341200FE000BF95 +:1074200094F84220A25CF25494F84210491CB1FB3B +:10743000F0F200FB12115B1C84F84210DBB2AB428D +:10744000EED3012700F0DDF83846BDE8F08172493F +:1074500010B5802081F800047049002081F84200B6 +:1074600081F84100433181F8420081F84100433105 +:1074700081F8420081F841006948FFF797FF6848AA +:10748000401CFFF793FFEBF793FCBDE8104000F0C2 +:10749000B8B840207047614800F0A7B80A460146D6 +:1074A0005E48AFE7402070475C48433000F09DB82D +:1074B0000A46014659484330A4E7402101700020A4 +:1074C000704710B504465548863000F08EF820709D +:1074D000002010BD0A460146504810B58630FFF71F +:1074E00091FF08B1002010BD42F2070010BD70B539 +:1074F0000C460646412900D9FFDF4A48006810388B +:1075000040B200F054F8C5B20D2000F050F8C0B2FF +:10751000854201D3012504E0002502E00DB1EBF71F +:107520008AFC224631463D48FFF76CFF0028F5D023 +:1075300070BD2DE9F0413A4F0025064617F10407CA +:1075400057F82540204600F041F810B36D1CEDB20D +:10755000032DF5D33148433000F038F8002825D00A +:107560002E4800F033F8002820D02C48863000F058 +:107570002DF800281AD0EBF734FC2948FFF71EFF3E +:10758000B0F5005F00D0FFDFBDE8F0412448FFF711 +:107590002BBF94F841004121265414F8410F401CA0 +:1075A000B0FBF1F201FB12002070D3E74DE7002899 +:1075B00004DB00F1E02090F8000405E000F00F008B +:1075C00000F1E02090F8140D4009704710F8411FB9 +:1075D0004122491CB1FBF2F302FB131140788142B6 +:1075E00001D1012070470020704710F8411F4078FA +:1075F000814201D3081A02E0C0F141000844C0B240 +:10760000704710B50648FFF7D9FE002803D1BDE842 +:107610001040EBF7D1BB10BD0DE000E0340B0020B3 +:10762000AC00002004ED00E070B5154D2878401C3A +:10763000C4B26878844202D000F0DBFA2C7070BDCE +:107640002DE9F0410E4C4FF0E02600BF00F0C6FAE5 +:107650000EF09AFB40BF20BF677820786070D6F8A4 +:107660000052E9F798FE854305D1D6F8040210B917 +:107670002078B842EAD000F0ACFA0020BDE8F081F2 +:10768000BC0000202DE9F04101264FF0E02231033B +:107690004FF000084046C2F88011BFF34F8FBFF390 +:1076A0006F8F204CC4F800010C2000F02EF81E4D06 +:1076B0002868C04340F30017286840F01000286095 +:1076C000C4F8046326607F1C02E000BF0EF05CFB80 +:1076D000D4F800010028F9D01FB9286820F0100064 +:1076E0002860124805686660C4F80863C4F8008121 +:1076F0000C2000F00AF82846BDE8F08110B50446D9 +:10770000FFF7C0FF2060002010BD002809DB00F05B +:107710001F02012191404009800000F1E020C0F8E3 +:107720008012704700C0004010ED00E008C5004026 +:107730002DE9F047FF4C0646FF21A06800EB06123A +:1077400011702178FF2910D04FF0080909EB0111C1 +:1077500009EB06174158C05900F0F4F9002807DD7D +:10776000A168207801EB061108702670BDE8F0874B +:1077700094F8008045460DE0A06809EB05114158DA +:10778000C05900F0DFF9002806DCA068A84600EB2D +:1077900008100578FF2DEFD1A06800EB061100EB73 +:1077A00008100D700670E1E7F0B5E24B04460020CA +:1077B00001259A680C269B780CE000BF05EB0017AA +:1077C000D75DA74204D106EB0017D7598F4204D0EA +:1077D000401CC0B28342F1D8FF20F0BD70B5FFF766 +:1077E000ECF9D44C08252278A16805EB02128958DF +:1077F00000F0A8F9012808DD2178A06805EB011147 +:107800004058BDE87040FFF7CFB9FFF7A1F8BDE8D9 +:107810007040EBF779BB2DE9F041C64C2578FFF7B6 +:10782000CCF9FF2D6ED04FF00808A26808EB0516C2 +:10783000915900F087F90228A06801DD80595DE0C8 +:1078400000EB051109782170022101EB0511425C62 +:107850005AB1521E4254815901F5800121F07F41F5 +:1078600081512846FFF764FF34E00423012203EB33 +:10787000051302EB051250F803C0875CBCF1000F42 +:1078800010D0BCF5007F10D9CCF3080250F806C028 +:107890000CEB423C2CF07F4C40F806C0C3589A1ABF +:1078A000520A09E0FF2181540AE0825902EB4C326E +:1078B00022F07F428251002242542846FFF738FFCF +:1078C0000C21A06801EB05114158E06850F8272011 +:1078D000384690472078FF2814D0FFF76EF92278B9 +:1078E000A16808EB02124546895800F02BF90128DF +:1078F00093DD2178A06805EB01114058BDE8F04107 +:10790000FFF752B9BDE8F081F0B51D4614460E46AA +:107910000746FF2B00D3FFDFA00700D0FFDF85481D +:10792000FF210022C0E90247C57006710170427054 +:1079300082701046012204E002EB0013401CE15467 +:10794000C0B2A842F8D3F0BD70B57A4C064665784F +:107950002079854200D3FFDFE06840F82560607839 +:10796000401C6070284670BD2DE9FF5F1D468B46A8 +:107970000746FF24FFF721F9DFF8B891064699F88A +:107980000100B84200D8FFDF00214FF001084FF09E +:107990000C0A99F80220D9F808000EE008EB011350 +:1079A000C35CFF2B0ED0BB4205D10AEB011350F88C +:1079B00003C0DC450CD0491CC9B28A42EED8FF2C6A +:1079C00002D00DE00C46F6E799F803108A4203D185 +:1079D000FF2004B0BDE8F09F1446521C89F8022035 +:1079E00008EB04110AEB0412475440F802B00421DA +:1079F000029B0022012B01EB04110CD040F8012066 +:107A00004FF4007808234FF0020C454513D9E905DF +:107A1000C90D02D002E04550F2E7414606EB413283 +:107A200003EB041322F07F42C250691A0CEB0412DC +:107A3000490A81540BE005B9012506EB453103EBFA +:107A4000041321F07F41C1500CEB0411425499F80A +:107A500000502046FFF76CFE99F80000A84201D0C4 +:107A6000FFF7BCFE3846B4E770B50C460546FFF795 +:107A7000A4F8064621462846FFF796FE0446FF284E +:107A80001AD02C4D082101EB0411A868415830464A +:107A900000F058F800F58050C11700EBD1404013BA +:107AA0000221AA6801EB0411515C09B100EB4120ED +:107AB000002800DC012070BD002070BD2DE9F047DA +:107AC00088468146FFF770FE0746FF281BD0194DF8 +:107AD0002E78A8683146344605E0BC4206D02646DA +:107AE00000EB06121478FF2CF7D10CE0FF2C0AD023 +:107AF000A6420CD100EB011000782870FF2804D0BA +:107B0000FFF76CFE03E0002030E6FFF753F8414634 +:107B10004846FFF7A9FF0123A968024603EB0413B7 +:107B2000FF20C854A878401EB84200D1A87001EBCD +:107B3000041001E0000C002001EB06110078087031 +:107B4000104613E6081A0002C11700EB116000127C +:107B50007047000010B5202000F07FF8202000F0D2 +:107B60008DF84D49202081F80004E9F712FC4B49BB +:107B700008604B48D0F8041341F00101C0F8041329 +:107B8000D0F8041341F08071C0F804134249012079 +:107B90001C39C1F8000110BD10B5202000F05DF8BF +:107BA0003E480021C8380160001D01603D4A481E62 +:107BB00010603B4AC2F80803384B1960C2F8000154 +:107BC000C2F8600138490860BDE81040202000F08C +:107BD00055B834493548091F086070473149334862 +:107BE000086070472D48C8380160001D521E0260B1 +:107BF00070472C4901200860BFF34F8F70472DE973 +:107C0000F0412849D0F8188028480860244CD4F85E +:107C100000010025244E6F1E28B14046E9F712FBF3 +:107C200040B9002111E0D4F8600198B14046E9F76D +:107C300009FB48B1C4F80051C4F860513760BDE891 +:107C4000F041202000F01AB831684046BDE8F0410C +:107C50000EF0A4B8FFDFBDE8F08100280DDB00F0D6 +:107C60001F02012191404009800000F1E020C0F88E +:107C70008011BFF34F8FBFF36F8F7047002809DB70 +:107C800000F01F02012191404009800000F1E02036 +:107C9000C0F880127047000020E000E0C8060240F3 +:107CA00000000240180502400004024001000001EB +:107CB0005E4800210170417010218170704770B5DD +:107CC000054616460C460220EAF714FE57490120E5 +:107CD00008705749F01E086056480560001F046090 +:107CE00070BD10B50220EAF705FE5049012008706A +:107CF00051480021C0F80011C0F80411C0F8081163 +:107D00004E494FF40000086010BD48480178D9B1D1 +:107D10004B4A4FF4000111604749D1F8003100226D +:107D2000002B1CBFD1F80431002B02D0D1F8081170 +:107D300019B142704FF0100104E04FF001014170A1 +:107D400040490968817002704FF00000EAF7D2BD27 +:107D500010B50220EAF7CEFD34480122002102705E +:107D60003548C0F80011C0F80411C0F808110260CD +:107D700010BD2E480178002904BF407870472E4876 +:107D8000D0F80011002904BF02207047D0F800117C +:107D900000291CBFD0F80411002905D0D0F8080133 +:107DA000002804BF01207047002070471F4800B51D +:107DB0000278214B4078C821491EC9B282B1D3F85C +:107DC00000C1BCF1000F10D0D3F8000100281CBF87 +:107DD000D3F8040100280BD0D3F8080150B107E014 +:107DE000022802D0012805D002E00029E4D1FFDFFB +:107DF000002000BD012000BD0C480178002904BF0F +:107E0000807870470C48D0F8001100291CBFD0F8CA +:107E10000411002902D0D0F8080110B14FF0100071 +:107E2000704708480068C0B270470000BE000020DC +:107E300010F5004008F5004000F0004004F5014056 +:107E400008F5014000F400405748002101704170DE +:107E5000704770B5064614460D460120EAF74AFD04 +:107E600052480660001D0460001D05605049002056 +:107E7000C1F850014F490320086050494E4808603E +:107E8000091D4F48086070BD2DE9F0410546464880 +:107E90000C46012606704B4945EA024040F08070CE +:107EA0000860FFF72CFA002804BF47480460002749 +:107EB000464CC4F80471474945480860002D02BF8C +:107EC000C4F800622660BDE8F081012D18BFFFDF15 +:107ED000C4F80072266041493F480860BDE8F0815F +:107EE0003148017871B13B4A394911603749D1F8BD +:107EF00004210021002A08BF417002D0384A1268CC +:107F0000427001700020EAF7F5BC2748017800298B +:107F100004BF407870472D48D0F80401002808BFFE +:107F200070472F480068C0B27047002808BF7047EC +:107F30002DE9F0471C480078002808BFFFDF234CDC +:107F4000D4F80401002818BFBDE8F0874FF00209FB +:107F5000C4F80493234F3868C0F30018386840F021 +:107F600010003860D4F80401002804BF4FF4004525 +:107F70004FF0E02608D100BFC6F880520DF004FF94 +:107F8000D4F804010028F7D0B8F1000F03D1386805 +:107F900020F010003860C4F80893BDE8F0870B4962 +:107FA0000120886070470000C100002008F50040F3 +:107FB000001000401CF500405011004098F50140B1 +:107FC0000CF0004004F5004018F5004000F00040BF +:107FD0000000020308F501400000020204F5014020 +:107FE00000F4004010ED00E0012804BF41F6A47049 +:107FF0007047022804BF41F288307047042804BF4C +:1080000046F218007047082804BF47F2A0307047B6 +:1080100000B5FFDF41F6A47000BD10B5FE48002496 +:1080200001214470047044728472C17280F825404A +:10803000C462846380F83C4080F83D40FF2180F8B2 +:108040003E105F2180F83F1018300DF09FFFF3497C +:10805000601E0860091D0860091D0C60091D08608C +:10806000091D0C60091D0860091D0860091D0860D4 +:10807000091D0860091D0860091D0860091D0860C8 +:10808000091D0860091D086010BDE549486070477A +:10809000E448016801F00F01032904BF0120704783 +:1080A000016801F00F01042904BF02207047016834 +:1080B00001F00F01052904D0006800F00F00062828 +:1080C00007D1D948006810F0060F0CBF0820042023 +:1080D000704700B5FFDF012000BD012812BF022854 +:1080E00000207047042812BF08284FF4C87070475A +:1080F00000B5FFDF002000BD012804BF2820704725 +:10810000022804BF18207047042812BF08284FF423 +:10811000A870704700B5FFDF282000BD70B5C148CA +:10812000016801F00F01032908BF012414D0016880 +:1081300001F00F01042904BF022418210DD00168A9 +:1081400001F00F0105294BD0006800F00F00062850 +:108150001CBFFFDF012443D02821AF48C26A806AD8 +:10816000101A0E18082C04BF4EF6981547F2A030CE +:108170002DD02046042C08BF4EF628350BD0012800 +:1081800008BF41F6A47506D0022C1ABFFFDF41F6E6 +:10819000A47541F28835012C08BF41F6A47016D0B1 +:1081A000022C08BF002005D0042C1ABFFFDF0020DE +:1081B0004FF4C8702D1A022C08BF41F2883006D047 +:1081C000042C1ABFFFDF41F6A47046F21800281AEB +:1081D0004FF47A7100F2E730B0FBF1F0304470BD3B +:1081E0009148006810F0060F0CBF082404244FF4D7 +:1081F000A871B2E710B58D49026801F118040A634D +:1082000042684A63007A81F83800207E48B1207FB6 +:10821000F6F781F9A07E011C18BF0121207FF6F737 +:1082200069F9607E002808BF10BD607FF6F773F91A +:10823000E07E011C18BF0121607FBDE81040F6F709 +:1082400059B930B50024054601290AD0022908BFD2 +:108250004FF0807405D0042916BF08294FF0C74499 +:10826000FFDF44F4847040F480107149086045F4E5 +:10827000403001F1040140F00070086030BD30B5BD +:108280000024054601290AD0022908BF4FF0807456 +:1082900005D0042916BF08294FF0C744FFDF44F476 +:1082A000847040F480106249086045F4403001F168 +:1082B000040140F0007008605E48D0F8000100281A +:1082C00018BFFFDF30BD2DE9F04102274FF0E02855 +:1082D00001250024C8F88071BFF34F8FBFF36F8F63 +:1082E000554804600560FFF751F8544E18B13068E6 +:1082F00040F480603060FFF702F838B1306820F059 +:10830000690040F0960040F0004030604D494C4814 +:1083100008604FF01020806CB0F1FF3F04D04A4954 +:108320000A6860F317420A60484940F25B600860DF +:10833000091F40F203100860081F05603949032037 +:10834000086043480560444A42491160444A434931 +:108350001160121F43491160016821F440710160EE +:10836000016841F480710160C8F8807231491020C1 +:10837000C1F80403284880F83140C46228484068A6 +:10838000002808BFBDE8F081BDE8F0410047274A5A +:108390000368C2F81A308088D08302F11800017295 +:1083A00070471D4B10B51A7A8A4208D10146062241 +:1083B000981CF6F715F8002804BF012010BD002016 +:1083C00010BD154890F825007047134A5170107081 +:1083D0007047F0B50546800000F1804000F5805000 +:1083E0008B88C0F820360B78D1F8011043EA0121C0 +:1083F000C0F8001605F10800012707FA00F61A4C2C +:10840000002A04BF2068B04304D0012A18BFFFDF50 +:1084100020683043206029E0280C0020000E004036 +:10842000C40000201015004014140040100C00205F +:108430001415004000100040FC1F00403C17004095 +:108440002C000089781700408C150040381500403A +:108450005016004000000E0408F501404080004026 +:10846000A4F501401011004040160040206807FAB2 +:1084700005F108432060F0BD0CF0C4BCFE4890F844 +:1084800032007047FD4AC17811600068FC49000263 +:1084900008607047252808BF02210ED0262808BF93 +:1084A0001A210AD0272808BF502106D00A2894BFD5 +:1084B0000422062202EB4001C9B2F24A1160F249DD +:1084C000086070472DE9F047EB4CA17A012956D09E +:1084D000022918BFBDE8F087627E002A08BFBDE808 +:1084E000F087012950D0E17E667F0D1C18BF012561 +:1084F0005FF02401DFF894934FF00108C9F84C8035 +:10850000DFF88CA34718DAF80000B84228BFFFDF75 +:108510000020C9F84C01CAF80070300285F0010152 +:1085200040EA015040F0031194F82000820002F16B +:10853000804202F5C042C2F81015D64901EB800115 +:10854000A07FC20002F1804202F5F832C2F8141591 +:10855000D14BC2F81035E27FD30003F1804303F51D +:10856000F833C3F81415CD49C3F8101508FA00F014 +:1085700008FA02F10843CA490860BDE8F087227E84 +:10858000002AAED1BDE8F087A17E267F002914BF66 +:10859000012500251121ADE72DE9F041C14E8046AE +:1085A00003200D46C6F80002BD49BF4808602846B2 +:1085B0000CF02CFCB04F0124B8F1000F04BFBC72CA +:1085C000346026D0B8F1010F23D1B848006860B9F3 +:1085D00015F00C0F09D0C6F80443012000F0DAFEB4 +:1085E000F463346487F83C4002E0002000F0D2FEDF +:1085F00028460CF0F3FC0220B872FEF7B7FE38B93B +:10860000FEF7C4FE20B9AA48016841F4C021016008 +:1086100074609E48C4649E4800682946BDE8F041E5 +:1086200050E72DE9F0479F4E814603200D46C6F8DE +:108630000002DFF86C829C48C8F8000008460CF085 +:10864000E5FB28460CF0CAFC01248B4FB9F1000F62 +:1086500003D0B9F1010F0AD031E0BC72B86B40F41D +:108660008010B8634FF48010C8F8000027E00220A3 +:10867000B872B86B40F40010B8634FF40010C8F83B +:1086800000008A48006860B915F00C0F09D0C6F8E0 +:108690000443012000F07EFEF463346487F83C401C +:1086A00002E0002000F076FEFEF760FE38B9FEF72B +:1086B0006DFE20B97E48016841F4C0210160EAF7EF +:1086C000F7FA2946BDE8F047FCE62DE9F84F754C6E +:1086D0008246032088461746C4F80002DFF8C0919E +:1086E0007148C9F8000010460CF090FBDFF8C4B1E7 +:1086F000614E0125BAF1000F04BFCBF80040B572FE +:1087000004D0BAF1010F18BFFFDF2FD06A48C0F8BC +:1087100000806B4969480860B06B40F40020B0638A +:10872000D4F800321021C4F808130020C4F8000265 +:10873000DFF890C18A03CCF80020C4F80001C4F827 +:108740000C01C4F81001C4F80401C4F81401C4F801 +:1087500018015D4800680090C4F80032C9F8002094 +:10876000C4F80413BAF1010F14D026E038460CF017 +:1087700035FCFEF7FBFD38B9FEF708FE20B94C4882 +:10878000016841F4C02101605048CBF8000002208C +:10879000B072BBE74548006860B917F00C0F09D00C +:1087A000C4F80453012000F0F5FDE563256486F864 +:1087B0003C5002E0002000F0EDFD4FF40020C9F82D +:1087C00000003248C56432480068404528BFFFDFDA +:1087D00039464046BDE8F84F74E62DE9F041264C95 +:1087E0000646002594F8310017468846002808BF41 +:1087F000FFDF16B1012E16D021E094F831000128D8 +:1088000008D094F83020394640460CF014FBE16A59 +:10881000451814E094F830103A4640460CF049FBF5 +:10882000E16A45180BE094F8310094F83010012803 +:108830003A46404609D00CF064FBE16A45183A46D6 +:1088400029463046BDE8F0413FE70CF014FBE16AF1 +:108850004518F4E72DE9F84F124CD4F8000220F047 +:108860000309D4F804034FF0100AC0F30018C4F849 +:1088700008A300262CE00000280C0020241500404E +:108880001C150040081500405415004000800040B1 +:108890004C850040006000404C81004010110040B9 +:1088A00004F5014000100040000004048817004057 +:1088B00068150040ACF50140488500404881004003 +:1088C000A8F5014008F501401811004004100040CF +:1088D000C4F80062FC48FB490160FC4D0127A97AFD +:1088E000012902D0022903D015E0297E11B912E036 +:1088F000697E81B1A97FEA7F07FA01F107FA02F2E6 +:108900001143016095F82000800000F1804000F5DF +:10891000C040C0F81065FF208DF80000C4F8106159 +:10892000276104E09DF80000401E8DF800009DF8CE +:10893000000018B1D4F810010028F3D09DF8000011 +:10894000002808BFFFDFC4F81061002000F022FDFE +:108950006E72AE72EF72C4F80092B8F1000F18BFD9 +:10896000C4F804A3BDE8F88FFF2008B58DF8000017 +:10897000D7480021C0F810110121016105E000BFB6 +:108980009DF80010491E8DF800109DF8001019B1D7 +:10899000D0F810110029F3D09DF80000002808BF7E +:1089A000FFDF08BD0068CB4920F07F4008607047BA +:1089B0004FF0E0200221C0F8801100F5C070BFF335 +:1089C0004F8FBFF36F8FC0F80011704710B490E85D +:1089D0001C10C14981E81C10D0E90420C1E9042021 +:1089E00010BC70474FF0E0210220C1F80001704731 +:1089F000BA4908707047BA490860704770B50546B3 +:108A0000EAF756F9B14C2844E16A884298BFFFDF83 +:108A100001202074EAF74CF9B24A28440021606131 +:108A2000C2F84411B0490860A06BB04940F480001E +:108A3000A063D001086070BD70B5A44C0546AC4A77 +:108A40000220207410680E4600F00F00032808BFB3 +:108A5000012213D0106800F00F00042808BF022282 +:108A60000CD0106800F00F0005281BD0106800F033 +:108A70000F0006281CBFFFDF012213D094F831003D +:108A800094F83010012815D028460CF081FA954949 +:108A900060610020C1F844016169E06A08449249BC +:108AA000086070BD9348006810F0060F0CBF0822E4 +:108AB0000422E3E7334628460CF038FAE7E7824918 +:108AC0004FF4800008608148816B21F4800181634C +:108AD000002101747047C20002F1804202F5F832B1 +:108AE000854BC2F81035C2F81415012181407F482A +:108AF00001607648826B1143816370477948012198 +:108B00004160C1600021C0F84411774801606F489E +:108B1000C1627047794908606D48D0F8001241F091 +:108B20004001C0F8001270476948D0F8001221F0E7 +:108B30004001C0F800127149002008607047644885 +:108B4000D0F8001221F01001C0F80012012181615B +:108B500070475E49FF2081F83E005D480021C0F863 +:108B60001C11D0F8001241F01001C0F8001270473B +:108B7000574981B0D1F81C21012A0DD0534991F8F1 +:108B80003E10FF290DBF00204942017001B008BF0F +:108B90007047012001B07047594A126802F07F0205 +:108BA000524202700020C1F81C0156480068009033 +:108BB000EFE7F0B517460C00064608BFFFDF434D50 +:108BC00014F0010F2F731CBF012CFFDF002E0CBF10 +:108BD000012002206872EC7201281CBF0228FFDF0E +:108BE000F0BD3A4981F83F007047384A136C036082 +:108BF000506C086070472DE9F84F38480078042819 +:108C000028BFFFDF314CDFF8C080314D94F83C00C5 +:108C100000260127E0B1D5F8040110F1000918BFC2 +:108C20004FF00109D5F81001002818BF012050EAC3 +:108C300009014FF4002B17D08021C5F80813C8F89C +:108C400000B084F83C6090F0010F18BFBDE8F88FC9 +:108C5000DFF89090D9F84C01002871D0A07A012853 +:108C60006FD002286ED0D1E0D5F80001DFF890A0D7 +:108C700030B3C5F800616F61FF20009002E0401E34 +:108C8000009005D0D5F81C0100280098F7D000B955 +:108C9000FFDFDAF8000000F07F0A94F83F0050454B +:108CA0003CBF002000F076FB84F83EA0C5F81C61B4 +:108CB000C5F808731348006800902F64AF6326E07E +:108CC00022E0000000000E0408F50140280C0020FE +:108CD000001000403C150040100C0020C400002093 +:108CE00004150040008000404485004004F5014028 +:108CF000101500401414004004110040601500409D +:108D0000481500401C110040B9F1000F03D0B9F123 +:108D1000000F2ED05CE0DAF8000000F07F0084F84D +:108D20003E00C5F81C6194F83D1061B194F83F1005 +:108D300081421BD2002000F02DFB2F64AF6315E0B1 +:108D400064E04CE04EE0FE49096894F83F308AB296 +:108D5000090C984203D30F2A06D9022904D2012014 +:108D600000F018FB2F6401E02F64AF63F548006842 +:108D700000908022C5F80423F3498F64F348036808 +:108D8000A0F1040CDCF800C043F698273B4463458F +:108D900015D2026842F210731A440260C1F84861A9 +:108DA000EC49EB480860091FEB480860EB48C0F845 +:108DB00000B0A06B40F40020A063BDE8F88F06600F +:108DC000C1F84861C5F80823C8F800B0C1F8486187 +:108DD0008020C5F80803C8F800B0BDE8F88F207EF1 +:108DE00010B913E0607E88B1A07FE17F07FA00F040 +:108DF00007FA01F10843C8F8000094F82000800049 +:108E000000F1804000F5C040C0F81065C9F84C7012 +:108E1000D34800682064D34800686064D248A16BDE +:108E20000160A663217C002019B1D9F84411012901 +:108E300000D00021A27A012A6ED0022A74D000BF8D +:108E4000D5F8101101290CBF1021002141EA0008BA +:108E5000C648016811F0FF0F03D0D5F8141101299D +:108E600000D0002184F83210006810F0FF0F03D00A +:108E7000D5F81801012800D0002084F83300BC4840 +:108E8000006884F83400FEF774FF012818BF002042 +:108E900084F83500C5F80061C5F80C61C5F81061AB +:108EA000C5F80461C5F81461C5F81861B1480068D7 +:108EB0000090A548C0F84461AF480068DFF8BC9254 +:108EC0000090D9F80000A062A9F104000068E062F7 +:108ED000AB48016801F00F01032908BF012013D03E +:108EE000016801F00F01042908BF02200CD00168BD +:108EF00001F00F01052926D0006800F00F000628B8 +:108F00001CBFFFDF01201ED084F83000A07A84F857 +:108F1000310002282CD11EE0D5F80C01012814BF25 +:108F2000002008208CE7FFE7D5F80C01012814BFCA +:108F300000200220934A1268012A14BF0422002252 +:108F4000104308437CE79048006810F0060F0CBF00 +:108F500008200420D8E7607850B18C490968097866 +:108F60000840217831EA000008BF84F8247001D05D +:108F700084F82460DFF818A218F0020F06D0E9F791 +:108F800097FEA16A081ADAF81010884718F0010F46 +:108F900018BF4FF0000B0DD0E9F78AFEE16ADAF84E +:108FA0001420081A594690477A48007810F0010FAB +:108FB0002FD10CE018F0020F18BF4FF0010BEBD1CE +:108FC00018F0080F18BF4FF0020BE5D1ECE7DFF8FF +:108FD000BCB1DBF80000007800F00F00072828BFC4 +:108FE00084F8256015D2DBF80000062200F10901A3 +:108FF000A01CF5F7F5F940B9207ADBF800100978E4 +:10900000B0EBD11F08BF012001D04FF0000084F861 +:109010002500E17A4FF0000011F0020F1CBF18F09C +:10902000020F18F0040F19D111F0100F1CBF94F8A3 +:109030003320002A02D094F835207AB111F0080FBD +:109040001CBF94F82420002A08D111F0040F02D08C +:1090500094F8251011B118F0010F01D04FF0010064 +:10906000617A19B168B1FFF7F5FB10E03E484A4953 +:109070000160D5F8000220F00300C5F80002E77295 +:1090800005E001290AD0022918BFFFDF0DD018F032 +:10909000010F14D0DAF80000804745E06672E772ED +:1090A000A7729621227B002006E06672E7720220FA +:1090B000A072227B96210120FFF78FFBE7E718F0D3 +:1090C000020F2AD018F0040F21D1FEF74FF9F0B9A2 +:1090D000FEF75CF9D8B931480168001F0068C0F399 +:1090E000006CC0F3425500F00F03C0F30312C0F34D +:1090F0000320BCF1000F0AD0002B1CBF002A00285F +:1091000005D1002918BF032D38BF48F0040827EA0D +:109110009800DAF80410884706E018F0080F18BF26 +:10912000DAF8080056D08047A07A022818BFBDE8B8 +:10913000F88F207C002808BFBDE8F88F02492FE097 +:10914000741500401C11004000800040488500401C +:1091500014100040ACF501404881004004F5014086 +:1091600004B500404C85004008F501404016004021 +:109170001014004018110040448100404485004014 +:109180001015004000140040141400400415004065 +:10919000100C0020C40000200000040454140040FF +:1091A000C1F8446102281DD0012818BFFFDFE16A21 +:1091B0006069884298BFFFDFD4F81400C9F8000046 +:1091C000A06B4FF4800140F48000A06382480160EE +:1091D000BDE8F88F18F0100F14BFDAF80C00FFDFAD +:1091E000A1D1A1E76169E06A0844E7E738B57B49A6 +:1091F00004460220887201212046FFF763F9784A6D +:1092000004F13D001060774B0020C3F8440176491B +:10921000C1F80001C1F80C01C1F81001C1F8040146 +:10922000C1F81401C1F818017048006800900120CD +:109230009864101D00681168884228BFFFDF38BDA0 +:109240002DE9F843654A88460024917A0125684F44 +:10925000012902D0022903D015E0117E11B912E0D4 +:10926000517E81B1917FD37F05FA01F105FA03F3B5 +:109270001943396092F82010890001F1804101F50D +:10928000C041C1F8104506460220907201213046C7 +:10929000FFF718F9524906F13D000860514AC2F83B +:1092A00044415148C0F80041C0F80C41C0F8104199 +:1092B000C0F80441C0F81441C0F818414B48006898 +:1092C00000909564081D00680968884228BFFFDF88 +:1092D000B8F1000F1CBF4FF400303860BDE8F883D0 +:1092E000022810B50DD0012804BF42F6CE3010BDC3 +:1092F000042817BF082843F6A440FFDF41F66A00A0 +:1093000010BDFDF7E5FF30B9FDF7F9FF002808BFF4 +:1093100041F6583001D041F2643041F29A010844DC +:1093200010BD314910B50020C1F800023049314864 +:109330000860324930480860091D31480860091D3D +:1093400030480860091D30480860091D2F48086032 +:10935000091D2F48086001200BF058FD1E494FF4ED +:109360003810086010BD22494FF43810086070476B +:109370002848016803291BBF00680228012000203B +:109380007047244801680B291BBF00680A28012088 +:109390000020704720490968C9B9204A204913684C +:1093A00070B123F0820343F07D0343F00043136068 +:1093B0000A6822F0100242F0600242F0004205E02A +:1093C00023F0004313600A6822F000420A60034958 +:1093D00081F83D007047000004F50140280C002092 +:1093E00044850040008000400010004018110040FB +:1093F00008F50140000004041011004098F50140F8 +:109400000410004044810040141000401C11004032 +:109410001010004050150040881700403C170040D5 +:109420007C17004010B5404822220021F5F72FF8A4 +:109430003D480024017821F010010170012104F061 +:10944000FFFE3A494FF6FF7081F822408884384980 +:109450000880488010BD704734498A8C824218BF0A +:109460007047002081F822004FF6FF708884704713 +:109470002D49016070472E49088070472B498A8C1E +:10948000A2F57F43FF3B03D00021016008467047EF +:1094900091F822202549012A1ABF016001200020ED +:1094A0007047224901F1220091F82220012A04BFCD +:1094B00000207047012202701D48008888841046F1 +:1094C00070471B49488070471849194B8A8C5B8844 +:1094D0009A4206D191F82220002A1EBF0160012085 +:1094E0007047002070471148114A818C5288914280 +:1094F00009D14FF6FF71818410F8221F19B10021A4 +:10950000017001207047002070470848084A818C8C +:109510005288914205D190F8220000281CBF0020FB +:109520007047012070470000960C0020700C00204E +:10953000CC0000207047584A012340B1012818BFD1 +:1095400070471370086890608888908170475370E6 +:109550000868C2F802008888D08070474E4A10B16F +:10956000012807D00EE0507860B1D2F80200086000 +:10957000D08804E0107828B19068086090898880CD +:109580000120704700207047434910B1012803D0E3 +:1095900006E0487810B903E0087808B10120704768 +:1095A0000020704730B58DB00C4605460D220021D5 +:1095B00004A8F4F76CFFE0788DF81F0020798DF88F +:1095C0001E0060798DF81D00286800906868019081 +:1095D000A8680290E868039068460AF087FF207840 +:1095E0009DF82F1088420CD160789DF82E1088428B +:1095F00007D1A0789DF82D10884202BF01200DB040 +:1096000030BD00200DB030BD30B50C4605468DB0E4 +:109610004FF0030104F1030012B1FDF749FF01E02F +:10962000FDF765FF60790D2220F0C00040F040009A +:109630006071002104A8F4F72AFFE0788DF81F007C +:1096400020798DF81E0060798DF81D002868009043 +:1096500068680190A8680290E868039068460AF07C +:1096600045FF9DF82F0020709DF82E0060709DF83A +:109670002D00A0700DB030BD10B5002904464FF08C +:10968000060102D0FDF714FF01E0FDF730FF60791D +:1096900020F0C000607110BDD0000020FE4840687E +:1096A00070472DE9F0410F46064601461446012059 +:1096B00005F06FF8054696F86500FEF795FC4AF24E +:1096C000B12108444FF47A71B0FBF1F0718840F297 +:1096D00071225143C0EB4100001BA0F55A7402F007 +:1096E0005AFF002818BF1E3CAF4234BF28463846F8 +:1096F000A04203D2AF422CBF3C462C467462BDE868 +:10970000F0812DE9FF4F8BB0044690F86500884644 +:109710000390DDE90D1008430A90E0480027057822 +:109720000C2D28BFFFDFDE4E36F8159094F88851D7 +:109730000C2D28BFFFDFDA4830F81500484480B20E +:10974000009094F87D000D280CBF012000200790A8 +:109750000D98002804BF94F82C0103282BD10798FA +:1097600048B3B4F8AA01404525D1D4F83401C4F86F +:109770002001608840F2E2414843C4F82401B4F873 +:109780007A01B4F806110844C4F82801204602F012 +:109790000CFFB4F8AE01E08294F8AC016075B4F847 +:1097A000B0016080B4F8B201A080B4F8B401E080E8 +:1097B000022084F82C01D4F884010990D4F88001A7 +:1097C0000690B4F80661B4F87801D4F874110191E8 +:1097D0000D9921B194F8401151B100F0D6B804F5BB +:1097E000807104917431059104F5B075091D07E08D +:1097F00004F5AA710491091D059104F5A275091DCE +:109800000891B4F87010A8EB0000A8EB01010FFA62 +:1098100080F90FFA81FBB9F1000F05DAD4F8700175 +:1098200001900120D9460A909C484FF0000A007927 +:10983000A8B3F2F77FFB90B3B4F8180102282ED337 +:1098400094F82C0102282AD094F8430138BB94F8EC +:10985000880100900C2828BFFFDF9148009930F85C +:10986000110000F5C86080B2009094F82C01012826 +:109870007ED0608840F2E2414843009901F0E6F86A +:10988000D4F8342180B206EB0B01A1EB0901821A56 +:1098900001FB02AAC4F83401012084F8430194F8C2 +:1098A0002C01002865D0012800F01482022800F065 +:1098B0007181032818BFFFDF00F04782A7EB0A0180 +:1098C0000198FCF738F90599012640F271220860E9 +:1098D0000898A0F80080002028702E710598006874 +:1098E000A8606188D4F834015143C0EB41006B4952 +:1098F000A0F54E70C8618969814287BF04990860EC +:10990000049801600498616A0068084400F5D47006 +:10991000E86002F040FE10B1E8681E30E8606E7149 +:10992000B4F8F000A0EB080000B20028C4BF032088 +:109930006871079800280E9800F06982E0B100BFB6 +:10994000B4F8181100290CBF0020B4F81A01A4F8CB +:109950001A0194F81C21401C504388420CD26879AB +:10996000401E002808DD6E71B4F81A01401C01E0A9 +:109970000FE05AE0A4F81A010D98002800F06A825E +:1099800094F84001002800F061820FB00220BDE889 +:10999000F08F94F8800003283DD03F4894F865107C +:1099A00090F82C00F7F78DFDE18A40F271225143C7 +:1099B00000EB4100CDF80800D4F82401009901F033 +:1099C00045F8D4F82021D4F82811821A01FB02AA04 +:1099D000C4F820010099029801F038F8D4F8301149 +:1099E000C4F83001411A8A44608840F2E241484399 +:1099F000009901F02BF806EB0B01D4F82821A1EB1C +:109A00000901891AD4F83421C4F83401821A491E94 +:109A100001FB02AA40E7E18A40F27122D4F8240156 +:109A2000514300EB41000290C6E70698002808BFAA +:109A3000FFDF94F86510184890F82C00F7F741FD07 +:109A40000990E08A40F271214143099800EB4100FE +:109A5000009900F0FBFFC4F83001608840F2E24159 +:109A60004843009900F0F2FFC4F8340103A902A8AA +:109A7000FFF7BBF8DDE90160039FE9F7F0F8014665 +:109A80003046FDF769F800F10F06E9F711F9381AC9 +:109A9000801B009006E00000B80C0020E0000020D1 +:109AA000E4620200B4F83401214686B20120D4F801 +:109AB000289004F06EFE074694F86500FEF794FACD +:109AC0004AF2B12108444FF47A7BB0FBFBF0618885 +:109AD00040F271225143C0EB4100801BA0F55A7641 +:109AE00002F059FD002818BF1E3EB94534BF384664 +:109AF0004846B04203D2B9452CBF4E463E46666248 +:109B000094F86500FEF7E9FA00F2E140B0FBFBF1E2 +:109B100006980F1894F86500FEF7DFFA064694F8E9 +:109B20006500FEF761FA30444AF2AB310844B0FBFD +:109B3000FBF1E08A40F2712242430998D4F8306187 +:109B400000EB4200401A801B384400993138471A14 +:109B5000607D40F2E24110FB01F994F8650000904D +:109B600010F00C0F0ABF00984EF62830FEF73CFAB2 +:109B70004AF2B1210844B0FBFBF000EB460000EBD9 +:109B800009060098FEF7B8FA304400F18401FE4857 +:109B9000816193E6E18A40F27122D4F824015143B5 +:109BA00000EB4100009900F051FFC4F830016188DA +:109BB00040F2E2404843009900F048FFC4F8340105 +:109BC00087B221460120D4F828B004F0E2FD814696 +:109BD00094F86500FEF708FA4AF2B12101444FF407 +:109BE0007A70B1FBF0F0618840F271225143C0EB12 +:109BF0004100C01BA0F55A7702F0CDFC002818BF29 +:109C00001E3FCB4534BF48465846B84203D2CB45E9 +:109C10002CBF5F464F4667621EBB0E9808B394F890 +:109C200065603046FEF7E0F94AF2B12101444FF495 +:109C30007A70B1FBF0F0D4F83011E28A084440F2B7 +:109C40007123D4F824115A4301EB42010F1A304614 +:109C5000FEF752FA01460998401A3844A0F120074D +:109C60000AE0E18A40F27122D4F82401514300EB6A +:109C70004100D4F83011471AD4F82821D4F8201123 +:109C8000D4F8300101FB0209607D40F2E24110FB93 +:109C900001FB94F8656016F00C0F0ABF30464EF6D3 +:109CA0002830FEF7A1F94AF2B12101444FF47A704D +:109CB000B1FBF0F000EB490000EB0B093046FEF77A +:109CC0001BFA484400F16001AF488161012084F82B +:109CD0002C01F3E5618840F271225143D4F834013C +:109CE000D4F82821C0EB410101FB09F706EB0B0179 +:109CF000891AD4F820C1D4F83031491E0CFB023245 +:109D000001FB0029607D40F2E24110FB01FB94F869 +:109D1000656016F00C0F0ABF30464EF62830FEF78D +:109D200063F94AF2B12101444FF47A70B1FBF0F0CB +:109D300000EB490000EB0B093046FEF7DDF9484423 +:109D400000F1600190488161B8E5618840F27122BC +:109D5000D4F834015143C0EB410000FB09F794F8FB +:109D60007C0024281CBF94F87D0024280BD1B4F873 +:109D7000AA01A8EB000000B2002804DB94F8AD01B2 +:109D8000002818BF03900A9800B3FEB9099800286C +:109D90001ABF06980028FFDF94F8650010F00C0F3A +:109DA00014BF4EF62830FEF71FF94AF2B1210144E4 +:109DB0004FF47A70B1FBF0F03F1A94F86500FEF7AB +:109DC0009BF90999081A3844A0F12007D4F83411F6 +:109DD00006EB0B0000FB01F6039810F00C0F0ABF16 +:109DE00003984EF62830FEF7FFF84AF2B1210144FD +:109DF0004FF47A70B1FBF0F000EB46060398FEF7E3 +:109E00007BF9304400F160015F48816156E500282C +:109E10007FF496AD94F82C0100283FF4ADAD618835 +:109E200040F27122D4F834015143C0EB410128467D +:109E3000F7F712F90004000C3FF49EAD18990029C1 +:109E400018BF088001200FB0BDE8F08F94F87C01A6 +:109E5000FCF7D1FC94F87C012946FCF7B0FB20B15B +:109E60000D9880F0010084F841010FB00020BDE89A +:109E7000F08F2DE9F843454C0246434F00266168B8 +:109E8000606A052A60D2DFE802F003464B4F5600B5 +:109E9000A07A002560B101216846FDF709FB9DF815 +:109EA000000042F210710002B0FBF1F201FB12055A +:109EB000F4F720FE4119A069FBF73DFEA06126746E +:109EC000032060754FF0010884F81480607AD0B9DF +:109ED000A06A80B1F4F70EFE0544F4F785FC411941 +:109EE000A06A884224BF401BA06204D2C4F8288024 +:109EF000F5F72BFE07E0207B04F11001FCF75FFB78 +:109F0000002808BFFFDF2684FCF739F87879BDE820 +:109F1000F843E8F7F9BFBDE8F843002100F0B3BD0E +:109F2000C1F88001BDE8F883D1F88001BDE8F843AD +:109F3000012100F0A8BD84F83060FCF720F87879A2 +:109F4000BDE8F843E8F7E0BFFFDFBDE8F8832DE99F +:109F5000F04F0E4C824683B020788B4601270025B7 +:109F6000094E4FF00209032804BF207B50457DD1E4 +:109F7000606870612078032818BFFFDF4FF0030886 +:109F8000BBF1080F73D203E0E0000020B80C002002 +:109F9000DFE80BF0040F32322D9999926562F5F7E4 +:109FA000ABF9002818BFFFDF86F8028003B0BDE8D8 +:109FB000F08FF4F77AFF68B9F4F716FC0546E0690C +:109FC000A84228BFE56105D2281A0421FCF7F7FD55 +:109FD000E56138B1F5F723FD002818BFFFDF03B0B6 +:109FE000BDE8F08F03B00020BDE8F04F41E703B0BB +:109FF000BDE8F04FFEF7FFBD2775257494F83000DB +:10A000004FF0010A58B14FF47A71A069FBF793FD44 +:10A01000A061002104F11000F7F71EF80EE0F4F73C +:10A0200069FD82465146A069FBF785FDA061514656 +:10A0300004F11000F7F710F800F1010A208C411C20 +:10A040000A293CBF50442084606830B1208C401CF9 +:10A050000A2828BF84F8159001D284F81580607A08 +:10A06000A8B9A06AE8B1F4F745FD01E02FE02AE0C5 +:10A070008046F4F7B9FB00EB0801A06A884224BFD0 +:10A08000A0EB0800A0620CD2A762F5F75EFD207B72 +:10A09000FCF74BF82570707903B0BDE8F04FE8F796 +:10A0A00033BF207B04F11001FCF789FA002808BFB8 +:10A0B000FFDF03B0BDE8F08F207BFCF736F825709A +:10A0C00003B0BDE8F08FFFDF03B0BDE8F08FBAF159 +:10A0D000200F28BFFFDFDFF8E886072138F81A00D5 +:10A0E000F9F7E4FE040008BFFFDFBAF1200F28BF34 +:10A0F000FFDF38F81A002188884218BFFFDF4FF0D1 +:10A10000200A7461BBF1080F80F06181DFE80BF079 +:10A110000496A0A099FEFDFCC4F88051F580C4F817 +:10A12000845194F8410138B9FCF71EF8D4F84C1169 +:10A13000FCF712FD00281BDCB4F83E11B4F87000E7 +:10A14000814206D1B4F8F410081AA4F8F6002046AB +:10A1500005E0081AA4F8F600B4F83E112046A4F869 +:10A160007010D4F86811C4F84C11C0F870111DE0DB +:10A17000B4F83C11B4F87000081AA4F8F600B4F86A +:10A180003C112046A4F87010D4F84C11C4F86811A2 +:10A19000C4F87011D4F85411C4F80011D4F858114F +:10A1A000C4F87411B4F85C11A4F8781102F008F93D +:10A1B000FBF7B4FF804694F86500FDF715FF4AF2FF +:10A1C000B12108444FF47A71B0FBF1F0D4F83411A6 +:10A1D00040F27122084461885143C0EB4100A0F174 +:10A1E000300AB8F1B70F98BF4FF0B70821460120E9 +:10A1F00004F0CFFA4044AAEB0000A0F21D38A246BA +:10A200002146012004F0C5FADAF824109C3081427E +:10A2100088BF0D1AC6F80C80454528BF4546B56075 +:10A22000D4F86C01A0F5D4703061FCF762FC84F8BE +:10A23000407186F8029003B0BDE8F08F02F0A6F9F5 +:10A2400001E0FEF7D8FC84F8407103B0BDE8F08F60 +:10A25000FBF78AFFD4F8702101461046FCF77CFC1E +:10A2600048B1628840F27123D4F834115A43C1EBEB +:10A270004201B0FBF1F094F87D100D290FD0B4F835 +:10A280007010B4F83E210B189A42AEBF501C401C0F +:10A290000844A4F83E0194F8420178B905E0B4F806 +:10A2A0003E01401CA4F83E0108E0B4F83E01B4F8B9 +:10A2B000F410884204BF401CA4F83E01B4F87A01AF +:10A2C0000DF1040B401CA4F87A01B4F89A00B4F81C +:10A2D0009810401AB4F87010401E08441FFA80F914 +:10A2E0000BE000231A462046CDF800B0FFF709FA2C +:10A2F00068B3012818BFFFDF48D0B4F83E11A9EBBE +:10A30000010000B2002802E053E047E05FE0E8DA35 +:10A31000082084F88D0084F88C70204601F012FE2D +:10A3200084F82C5194F87C514FF6FF77202D00D300 +:10A33000FFDF28F8157094F87C01FBF7F6FE84F82F +:10A340007CA1707903B0BDE8F04FE8F7DDBDA06EE9 +:10A35000002804BF03B0BDE8F08FB4F83E01B4F8A4 +:10A360009420801A01B20029DCBF03B0BDE8F08F51 +:10A37000B4F86C000144491E91FBF0F189B201FB75 +:10A380000020A4F8940003B0BDE8F08FB4F83E01BB +:10A39000BDF804100844A4F83E01AEE7FEF7E4FA65 +:10A3A000FEF729FC4FF0E020C0F8809203B0BDE832 +:10A3B000F08F94F82C01042818BFFFDF84F82C518B +:10A3C00094F87C514FF6FF77202DB2D3B0E7FFDF32 +:10A3D00003B0BDE8F08F10B5FA4C207850B10120E1 +:10A3E0006072F5F7D8FB2078032805D0207A002882 +:10A3F00008BF10BD0C2010BD207BFCF7FCF9207BB2 +:10A40000FCF765FC207BFBF790FE002808BFFFDF10 +:10A410000020207010BD2DE9F04FEA4F83B038784E +:10A4200001244FF0000840B17C720120F5F7B3FB26 +:10A430003878032818BF387A0DD0DFF88C9389F864 +:10A44000034069460720F9F7BAFC002818BFFFDF70 +:10A450004FF6FF7440E0387BFCF7CDF9387BFCF712 +:10A4600036FC387BFBF761FE002808BFFFDF87F86A +:10A470000080E2E7029800281CBF90F82C11002908 +:10A480002AD00088A0421CBFDFF834A34FF0200B75 +:10A490003AD00721F9F70AFD040008BFFFDF94F85E +:10A4A0007C01FCF714FC84F82C8194F87C514FF665 +:10A4B000FF76202D28BFFFDF2AF8156094F87C0175 +:10A4C000FBF733FE84F87CB169460720F9F777FC87 +:10A4D000002818BFFFDF12E06846F9F74EFC00289D +:10A4E000C8D011E0029800281CBF90F82C11002958 +:10A4F00005D00088A0F57F41FF39CAD104E0684645 +:10A50000F9F73BFC0028EDD089F8038087F830800C +:10A5100087F80B8003B00020BDE8F08FAA4948718E +:10A520000020887001220A7048700A71C870A5491D +:10A53000087070E7A449087070472DE9F84FA14CE6 +:10A5400006460F462078002862D1A048FBF792FD0E +:10A55000207320285CD04FF00308666084F80080E8 +:10A56000002565722572AEB1012106F58E70FCF7EB +:10A57000BEFF0620F9F742FC81460720F9F73EFCB2 +:10A5800096F81C114844B1FBF0F200FB1210401C7D +:10A5900086F81C01FBF7C2FD40F2F651884238BF35 +:10A5A00040F2F65000F5A0701FFA80F9F4F7A2FA15 +:10A5B000012680B3A672F4F717F9E061FBF7D4FD2A +:10A5C000824601216846FCF769FF9DF8000042F2CF +:10A5D00010710002B0FBF1F201FB120000EB090167 +:10A5E0005046FBF7A8FAA762A061267584F815808B +:10A5F0002574207B04F11001FBF7E1FF002808BF60 +:10A60000FFDF25840020F5F7C6FA0020BDE8F88FAB +:10A610000C20BDE8F88FFFE7E761FBF7A5FD494691 +:10A62000FBF789FAA061A57284F830600120FDF77C +:10A6300054FD4FF47A7100F2E140B0FBF1F0381AAA +:10A64000A0F5AB60A5626063CFE75F4948707047D3 +:10A650005D49087170475B4810B5417A00291CBFFD +:10A66000002010BD816A51B990F8301039B1416AAB +:10A67000406B814203D9F5F768FA002010BD012034 +:10A6800010BD2DE9F041504C0646E088401CE080AA +:10A69000D4E902516078D6F8807120B13A46284654 +:10A6A000F6F705FD0546A068854205D02169281A00 +:10A6B00008442061FCF71DFAA560AF4209D896F85E +:10A6C0002C01012805D0E078002804BF0120BDE856 +:10A6D000F0810020BDE8F08110B504460846FDF782 +:10A6E00083FC4AF2B12108444FF47A71B0FBF1F0D7 +:10A6F00040F2E241614300F54E7081428CBF081A7E +:10A70000002010BD70B5044682B0002084F84001DE +:10A7100094F8FB00002807BF94F82C01032802B02E +:10A7200070BDFBF721FDD4F8702101461046FCF7FF +:10A7300013FA0028DCBF02B070BD628840F27123BA +:10A74000D4F834115A43C1EB4201B0FBF1F0B4F834 +:10A750007010401C0844A4F83C01B4F8F400B4F8AC +:10A760003C21801A00B20028DCBF02B070BD01207D +:10A7700084F84201B4F89A00B4F8982001AE801A27 +:10A78000401E084485B212E00096B4F83C11002344 +:10A7900001222046FEF7B5FF002804BF02B070BDBD +:10A7A000012815D0022812BFFFDF02B070BDB4F837 +:10A7B0003C01281A00B20028BCBF02B070BDE3E71C +:10A7C000F00C0020B80C0020E00000204F9F01009A +:10A7D000B4F83C01BDF804100844A4F83C01E6E7D5 +:10A7E000F8B50422002506295BD2DFE801F0072630 +:10A7F0000319192A044680F82C2107E00446C948A9 +:10A80000C078002818BF84F82C210AD0FBF7B7FBCA +:10A81000A4F87A51B4F87000A4F83E0184F84251CB +:10A82000F8BD0095B4F8F410012300222046FEF78D +:10A8300068FF002818BFFFDFE8E7032180F82C112C +:10A84000F8BD0646876AB0F83401314685B201206A +:10A8500003F09FFF044696F86500FDF7C5FB4AF23A +:10A86000B12108444FF47A71B0FBF1F0718840F2E5 +:10A8700071225143C0EB4100401BA0F55A7501F015 +:10A880008AFE002818BF1E3DA74234BF2046384626 +:10A89000A84228BF2C4602D2A74228BF3C46746279 +:10A8A000F8BDFFDFF8BD2DE9F05F9E4EB1780229BB +:10A8B00006BFF1880029BDE8F09F7469C4F88401DF +:10A8C00094F86500FDF718FCD4F88411081AB168F3 +:10A8D0000144B160F1680844F060746994F8430180 +:10A8E000002808BFBDE8F09F94F82C01032818BF8A +:10A8F000BDE8F09F94F8655037780C2F28BFFFDF34 +:10A90000894E36F8178094F888710C2F28BFFFDF26 +:10A9100036F81700404494F8888187B2B8F10C0FDC +:10A9200028BFFFDF36F8180000F5C8601FFA80F86E +:10A930002846FDF7E1FBD4F884114FF0000A0E1A07 +:10A9400015F00C0F0ABF28464EF62830FDF74CFBD9 +:10A950004FF47A7900F2E730B0FBF9F0361A284666 +:10A96000FDF7CAFBD4F8001115F00C0FA1EB000B9A +:10A970000ABF28464EF62830FDF736FB4AF2B121D1 +:10A980000844B0FBF9F0ABEB0000A0F160017943A3 +:10A99000B1FBF8F1292202EB50006031A0EB51022B +:10A9A00000EB5100B24201D8B04201D8F1F774FB7C +:10A9B000608840F2E2414843394600F047F8C4F865 +:10A9C000340184F843A1BDE8F09F70B505465548B1 +:10A9D00090F802C0BCF1020F07BF406900F5C074D7 +:10A9E000524800F12404002904BF256070BD4FF4D3 +:10A9F0007A7601290DD002291CBFFFDF70BD1046F9 +:10AA0000FEF76EFC00F2E140B0FBF6F0281A206081 +:10AA100070BD1846FDF761FB00F2E140B0FBF6F0B7 +:10AA2000281A206070BD4148007800281CBF002013 +:10AA3000704710B50720F9F7D3F980F0010010BD79 +:10AA40003A480078002818BF0120704730B5024608 +:10AA50000020002908BF30BDA2FB0110490A41EACD +:10AA6000C051400A4C1C40F100000022D4F1FF31DB +:10AA700040F2A17572EB000038BFFFDF04F5F4600F +:10AA8000B0FBF5F030BD2DE9F843284C0025814698 +:10AA900084F83050D4F8188084F82C10E5722570B2 +:10AAA0000127277239466068F5F792FD6168C1F8A1 +:10AAB0007081267B81F87C61C1F88091C1F8748136 +:10AAC000B1F80080202E28BFFFDF194820F816803B +:10AAD000646884F82C510023A4F878511A4619466A +:10AAE00020460095FEF70DFE002818BFFFDFC4F8D2 +:10AAF0002851C4F8205184F82C71A4F83E51A4F8D0 +:10AB00003C5184F84251B4F87000401EA4F8700023 +:10AB1000A4F87A51FBF733FA02484079BDE8F843CC +:10AB2000E8F7F2B9E0000020E4620200B80C00206F +:10AB3000F00C0020012804D0022805D0032808D1F9 +:10AB400005E0012907D004E0022904D001E004292E +:10AB500001D000207047012070472DE9F0410E46DA +:10AB6000044603F08AFC0546204603F08AFC0446AE +:10AB7000F6F770FBFB4F010015D0386990F86420A0 +:10AB80008A4210D090F8C0311BB190F8C2312342F4 +:10AB90001FD02EB990F85D30234201D18A4218D8D7 +:10ABA00090F8C001A8B12846F6F754FB70B1396996 +:10ABB00091F86520824209D091F8C00118B191F84E +:10ABC000C301284205D091F8C00110B10120BDE8B1 +:10ABD000F0810020FBE730B5E24C85B0E069002849 +:10ABE0005FD0142200216846F3F751FC206990F8E9 +:10ABF0006500FDF7F9F94FF47A7100F5FA70B0FBD2 +:10AC0000F1F5206990F86500FDF776FA2844ADF873 +:10AC1000060020690188ADF80010B0F87010ADF89A +:10AC200004104188ADF8021090F8A20130B1A0697B +:10AC3000C11C039103F002FB8DF81000206990F80D +:10AC4000A1018DF80800E169684688472069002164 +:10AC500080F8A21180F8A1110399002921D090F861 +:10AC6000A01100291DD190F87C10272919D09DF83A +:10AC70001010039A002914D013780124FF2B12D04E +:10AC8000072B0ED102290CD15178FF2909D100BF21 +:10AC900080F8A0410399C0F8A4119DF8101080F825 +:10ACA000A31105B030BD1B29F2D9FAE770B5AD4C40 +:10ACB000206990F87D001B2800D0FFDF2069002567 +:10ACC00080F8A75090F8D40100B1FFDF206990F818 +:10ACD000A81041B180F8A8500188A0F8D81180F8D8 +:10ACE000D6510E2108E00188A0F8D81180F8D6517D +:10ACF000012180F8DA110D2180F8D4110088F9F7CC +:10AD000006FAF8F79FFE2079E8F7FEF8206980F848 +:10AD10007D5070BD70B5934CA07980072CD5A0787C +:10AD2000002829D162692046D37801690D2B01F1F1 +:10AD300070005FD00DDCA3F102034FF001050B2B77 +:10AD400019D2DFE803F01A1844506127182C183A7A +:10AD50006400152B6FD008DC112B4BD0122B5AD06E +:10AD6000132B62D0142B06D166E0162B71D0172B53 +:10AD700070D0FF2B6FD0FFDF70BD91F87F200123D3 +:10AD80001946F6F7FFF80028F6D12169082081F866 +:10AD90007F0070BD1079BDE8704001F090BC91F863 +:10ADA0007E00C00700D1FFDF01F048FC206910F8E9 +:10ADB0007E1F21F00101017070BD91F87D00102807 +:10ADC00000D0FFDF2069112180F8A75008E091F83A +:10ADD0007D00142800D0FFDF2069152180F8A750DE +:10ADE00080F87D1070BD91F87D00152800D0FFDF40 +:10ADF000172005E091F87D00152800D0FFDF19200D +:10AE0000216981F87D0070BDBDE870404EE7BDE866 +:10AE1000704001F028BC91F87C2001230021F6F756 +:10AE2000B1F800B9FFDF0E200FE011F87E0F20F01F +:10AE3000040008701DE00FE091F87C200123002140 +:10AE4000F6F7A0F800B9FFDF1C20216981F87C002B +:10AE500070BD12E01BE022E091F87E00C0F301100B +:10AE6000012800D0FFDF206910F87E1F21F01001BB +:10AE70000170BDE8704001F0E1BB91F87C20012336 +:10AE80000021F6F77FF800B9FFDF1F20DDE791F81A +:10AE90007D00212801D000B1FFDF2220B0E7BDE80E +:10AEA000704001F0D7BB2F48016991F87E2013074D +:10AEB00002D501218170704742F0080281F87E209E +:10AEC0008069C07881F8E10001F0AFBB10B5254C76 +:10AED00021690A88A1F8162281F8140291F8640009 +:10AEE00001F091FB216981F8180291F8650001F0E9 +:10AEF0008AFB216981F81902012081F812020020E1 +:10AF000081F8C0012079BDE81040E7F7FDBF10B51A +:10AF1000144C05212069FFF763FC206990F85A1052 +:10AF2000012908D000F5F57103F001FC2079BDE896 +:10AF30001040E7F7E9BF022180F85A1010BD10B5A4 +:10AF4000084C01230921206990F87C207030F6F725 +:10AF500019F848B12169002001F8960F087301F82B +:10AF60001A0C10BD000100200120A070F9E770B597 +:10AF7000F74D012329462869896990F87C200979D1 +:10AF80000E2A01D1122903D000241C2A03D004E088 +:10AF9000BDE87040D3E7142902D0202A07D008E08A +:10AFA00080F87C4080F8A240BDE87040AFE71629E9 +:10AFB00006D0262A01D1162902D0172909D00CE083 +:10AFC00000F87C4F80F82640407821280CD01A20C9 +:10AFD00017E090F87D20222A07D0EA69002A03D0E2 +:10AFE000FF2901D180F8A23132E780F87D4001F0DD +:10AFF00025FB286980F8974090F8C0010028F3D01D +:10B000000020BDE8704061E710B5D14C216991F88E +:10B010007C10202902D0262902D0A2E7FFF756FF94 +:10B020002169002081F87C0081F8A20099E72DE9D0 +:10B03000F843C74C206990F87C10202908D00027DD +:10B0400090F87D10222905D07FB300F17C0503E044 +:10B050000127F5E700F17D0510F8B01F41F004016C +:10B060000170A06903F015FA4FF00108002608B33B +:10B070003946A069FFF771FDE0B16A46A169206910 +:10B08000F6F7F7F890B3A06903F001FA2169A1F887 +:10B09000AA01B1F8701001F0AAFA40B32069282182 +:10B0A00080F88D1080F88C8058E0FFE70220A070B7 +:10B0B000BDE8F883206990F8C00110B11E20FFF7A9 +:10B0C00005FFAFB1A0692169C07881F8E20008FAF4 +:10B0D00000F1C1F3006000B9FFDF20690A2180F8A8 +:10B0E0007C1090F8A20040B9FFDF06E009E02AE0FA +:10B0F0002E7001F0A3FAFFF7D6FE206980F8976062 +:10B10000D6E7226992F8C00170B1B2F8703092F8B7 +:10B110006410B2F8C40102F5D572F6F79BF968B174 +:10B120002169252081F87C00206900F17D0180F8EB +:10B1300097608D4212D180F87D600FE00020FFF70C +:10B14000C5FE2E70F0E720699DF8001080F8AC1164 +:10B150009DF8011080F8AD1124202870206900F1BD +:10B160007D018D4203D1BDE8F84301F067BA80F854 +:10B17000A2609DE770B5764C01230B21206990F801 +:10B180007D207030F5F7FEFE202650BB206901239C +:10B19000002190F87D207030F5F7F4FE0125F0B124 +:10B1A000206990F87C0024281BD0A06903F04FF997 +:10B1B000C8B1206990F8B01041F0040180F8B010D7 +:10B1C000A1694A7902F0070280F85D20097901F04F +:10B1D000070180F85C1090F8C1311BBB06E0A57038 +:10B1E00036E6A67034E6BDE870405CE690F8C03103 +:10B1F000C3B900F164035E788E4205D1197891429B +:10B2000002D180F897500DE000F503710D700288AF +:10B210004A8090F85C200A7190F85D0048712079AE +:10B22000E7F772FE2169212081F87D00BDE87040BA +:10B2300001F0FBB9F8B5464C206990F87E0010F09B +:10B24000300F04D0A07840F00100A070F8BDA069D4 +:10B2500003F0E2F850B3A06903F0D8F80746A069FC +:10B2600003F0D8F80646A06903F0CEF80546A069B9 +:10B2700003F0CEF801460097206933462A46303065 +:10B2800003F0BFF9A079800703D56069C07814285E +:10B290000FD0216991F87C001C280AD091F85A003F +:10B2A00001280ED091F8B70158B907E0BDE8F84081 +:10B2B000F9E52169012081F85A0002E091F8B60110 +:10B2C00030B1206910F87E1F41F0100101700EE0CE +:10B2D00091F87E0001F5FC7240F0200081F87E00BC +:10B2E00031F8300B03F017FA2079E7F70DFEBDE8CF +:10B2F000F84001F09AB970B5154C206990F87E10AD +:10B30000890707D590F87C20012308217030F5F7D4 +:10B3100039FEF8B1206990F8AA00800712D4A0691C +:10B3200003F056F8216981F8AB00A06930F8052FC9 +:10B33000A1F8AC204088A1F8AE0011F8AA0F40F0A7 +:10B3400002000870206990F8AA10C90705D011E022 +:10B35000000100200120A0707AE590F87E008007AF +:10B3600000D5FFDF206910F87E1F41F00201017057 +:10B3700001F05BF92069002590F87C10062906D1C0 +:10B3800080F87C5080F8A2502079E7F7BDFD206955 +:10B3900090F8A8110429DFD180F8A8512079E7F7A7 +:10B3A000B3FD206990F87C100029D5D180F8A25017 +:10B3B0004EE570B5FB4C01230021206990F87D20FB +:10B3C0007030F5F7DFFD012578B9206990F87D2010 +:10B3D000122A0AD0012305217030F5F7D3FD10B1F0 +:10B3E0000820A07034E5A57032E5206990F8A80027 +:10B3F00008B901F01AF92169A06901F5847102F018 +:10B40000C8FF2169A069D83102F0CEFF206990F809 +:10B41000DC0100B1FFDF21690888A1F8DE0101F538 +:10B42000F071A06902F0A3FF2169A06901F5F47130 +:10B4300002F0A5FF206980F8DC51142180F87D100E +:10B440002079BDE87040E7F75FBD70B5D54C0123AA +:10B450000021206990F87D207030F5F793FD0125DB +:10B46000A8B1A06902F04FFF98B1A0692169B0F8B6 +:10B470000D00A1F8AA01B1F8701001F0B8F858B1A8 +:10B480002069282180F88D1080F88C50E0E4A570A8 +:10B49000DEE4BDE8704006E5A0692169027981F823 +:10B4A000AC21B0F80520A1F8AE2102F01FFF216900 +:10B4B000A1F8B001A06902F01CFF2169A1F8B20156 +:10B4C000A06902F01DFF2169A1F8B4010D2081F8E7 +:10B4D0007D00BDE47CB5B34CA079C00738D0A0692D +:10B4E00001230521C578206990F87D207030F5F79B +:10B4F00049FD68B1AD1E0A2D06D2DFE805F0090945 +:10B500000505090905050909A07840F00800A070A3 +:10B51000A07800281CD1A06902F0BEFE00286ED0E1 +:10B52000A0690226C5781DB1012D01D0162D18D1B4 +:10B53000206990F87C00F5F70DFD90B1216991F834 +:10B540007C001F280DD0202803D0162D16D0A67001 +:10B550007CBD262081F87C00162D02D02A20FFF722 +:10B56000B5FC0C2D5BD00CDC0C2D48D2DFE805F0CF +:10B5700036331F48BEBE4BB55ABE393C2020A070A2 +:10B580007CBD0120142D6ED008DC0D2D6CD0112D4A +:10B590006BD0122D6ED0132D31D168E0152D7FD0D8 +:10B5A000162D6FD0182D6ED0FF2D28D198E0206970 +:10B5B0000123194690F87F207030F5F7E3FC00284E +:10B5C00008D1A06902F0CCFE216981F88E01072024 +:10B5D00081F87F008CE001F0EDF889E0FFF735FF9E +:10B5E00086E001F0C7F883E0206990F87D1011290A +:10B5F00001D0A6707CE0122180F87D1078E075E023 +:10B60000FFF7D7FE74E0206990F87D001728F0D18D +:10B6100001F014F821691B2081F87D0068E0FFF734 +:10B620006AFE65E0206990F87E00C00703D0A0782C +:10B6300040F0010023E06946A06902F0D0FE9DF8C9 +:10B64000000000F02501206900F8B01F9DF80110EE +:10B6500001F04901417000F0E8FF206910F87E1FF9 +:10B6600041F0010117E018E023E025E002E0FFF7D8 +:10B6700066FC3DE0216991F87E10490704D5A07071 +:10B6800036E00DE00FE011E000F0CFFF206910F888 +:10B690007E1F41F0040101702AE0FFF7CBFD27E097 +:10B6A00001F030F824E0FFF765FD21E0FFF7BFFC73 +:10B6B0001EE0A06900790DE0206910F8B01F41F08C +:10B6C00004010170A06902F0F7FE162810D1A069EC +:10B6D00002F0F6FEFFF798FC0AE0FFF748FC07E0EF +:10B6E000E16919B1216981F8A20101E0FFF7DBFBF3 +:10B6F0002169F1E93002401C42F10002C1E9000277 +:10B700007CBD70B5274CA07900074AD5A0780028E9 +:10B7100047D1206990F8E400FE2800D1FFDF2069BE +:10B72000FE21002580F8E41090F87D10192906D13B +:10B7300080F8A75000F082FF206980F87D502069D2 +:10B7400090F87C101F2902D0272921D119E090F808 +:10B750007D00F5F7FFFB78B120692621012380F8F1 +:10B760007C1090F87D200B217030F5F70BFC78B938 +:10B770002A20FFF7ABFB0BE02169202081F87C0039 +:10B7800006E0012180F8A11180F87C5080F8A250D9 +:10B79000206990F87F10082903D10221217080F8D8 +:10B7A000E41021E40001002010B5FD4C216991F85E +:10B7B000AC210AB991F8642081F8642091F8AD2198 +:10B7C0000AB991F8652081F8652010B10020FFF7D3 +:10B7D0007DFB206902F041FF002806D02069BDE80A +:10B7E000104000F5F57102F0A2BF16E470B5EC4C04 +:10B7F00006460D46206990F8E400FE2800D0FFDFE1 +:10B800002269002082F8E46015B1A2F8A400E7E400 +:10B8100022F89E0F01201071E2E470B5E04C012384 +:10B820000021206990F87C207030F5F7ABFB0028F0 +:10B830007BD0206990F8B61111B190F8B71139B1E9 +:10B8400090F8C01100296FD090F8C11119B36BE0C6 +:10B8500090F87D1024291CD090F87C10242918D051 +:10B860005FF0000300F5D67200F5DB7102F096FE82 +:10B870002169002081F8B60101461420FFF7B6FFC8 +:10B88000216901F13000C28A21F8E62F408B4880FF +:10B8900050E00123E6E790F87D2001230B21703072 +:10B8A000F5F770FB68BB206990F8640000F0ABFE10 +:10B8B0000646206990F8650000F0A5FE054620695F +:10B8C00090F8C2113046FFF735F9D8B1206990F8E9 +:10B8D000C3112846FFF72EF9A0B12269B2F87030E3 +:10B8E00092F86410B2F8C40102F5D572F5F7B2FD12 +:10B8F00020B12169252081F87C001BE00020FFF7A2 +:10B90000E5FA11E020690123032190F87D207030D1 +:10B91000F5F738FB40B920690123022190F87D201A +:10B920007030F5F72FFB08B1002059E400211620F4 +:10B93000FFF75CFF012053E410B5E8BB984C206989 +:10B9400090F87E10CA0702D00121092052E08A0730 +:10B950000AD501210C20FFF749FF206910F8AA1F22 +:10B9600041F00101017047E04A0702D5012113208F +:10B9700040E00A0705D510F8E11F417101210720B9 +:10B9800038E011F0300F3BD090F8B711A1B990F822 +:10B99000B611E1B190F87D1024292FD090F87C10D9 +:10B9A00024292BD05FF0000300F5D67200F5DB717F +:10B9B00002F0F4FD216900E022E011F87E0F20F092 +:10B9C000200040F010000870002081F83801206944 +:10B9D00090F87E10C90613D502F03FFEFFF797FAE4 +:10B9E000216901F13000C28A21F8E62F408B48809E +:10B9F00001211520FFF7FAFE0120F6E60123D3E727 +:10BA00000020F2E670B5664C206990F8E410FE293B +:10BA100078D1A178002975D190F87F2001231946AB +:10BA20007030F5F7AFFA00286CD1206990F88C11CE +:10BA300049B10021A0F89C1090F88D1180F8E61013 +:10BA4000002102205BE090F87D200123042170306A +:10BA5000F5F798FA0546FFF76FFF002852D1284600 +:10BA600000F00CFF00284DD120690123002190F83F +:10BA70007C207030F5F786FA78B120690123042123 +:10BA800090F87D207030F5F77DFA30B9206990F894 +:10BA9000960010B10021122031E0206990F87C203E +:10BAA0000A2A0DD0002D2DD1012300217030F5F789 +:10BAB00069FA78B1206990F8A81104290AD105E043 +:10BAC00010F8E21F01710021072018E090F8AA0089 +:10BAD000800718D0FFF7A1FE002813D120690123A9 +:10BAE000002190F87C207030F5F74CFA002809D03E +:10BAF000206990F8A001002804D00021FF20BDE8B3 +:10BB0000704073E609E000210C20FFF76FFE20690A +:10BB100010F8AA1F41F0010101701DE43EB5054671 +:10BB20006846FDF7ABFC00B9FFDF22220021009838 +:10BB3000F2F7ADFC0321009802F096FB0098017823 +:10BB400021F010010170294602F0B3FB144C0D2DB9 +:10BB500043D00BDCA5F102050B2D19D2DFE805F06F +:10BB600022184B191922185718192700152D5FD0C4 +:10BB700008DC112D28D0122D0BD0132D09D0142D37 +:10BB800006D155E0162D2CD0172D6AD0FF2D74D07C +:10BB9000FFDFFDF786FC002800D1FFDF3EBD00007F +:10BBA000000100202169009891F8E61017E0E26892 +:10BBB00000981178017191884171090A8171518849 +:10BBC000C171090A0172E4E70321009802F072FCD6 +:10BBD0000621009802F072FCDBE700980621017153 +:10BBE000D7E70098D4F8101091F8C221027191F8AB +:10BBF000C3114171CDE72169009801F5887102F008 +:10BC0000D7FB21690098DC3102F0DCFBC1E7FA497F +:10BC1000D1E90001CDE90101206901A990F8B00046 +:10BC200000F025008DF80400009802F006FCB0E753 +:10BC30002069B0F84810009802F0D6FB2069B0F8EF +:10BC4000E810009802F0D4FB2069B0F84410009886 +:10BC500002F0D2FB2069B0F8E610009802F0D0FBA9 +:10BC600097E7216991F8C00100280098BCD111F82C +:10BC7000642F02714978BCE7FFE7206990F8A3219F +:10BC8000D0F8A411009802F022FB82E7DB4810B53F +:10BC9000006990F8821041B990F87D2001230621B7 +:10BCA0007030F5F76FF9002800D001209DE570B5E0 +:10BCB000D24D286990F8801039B1012905D00229A8 +:10BCC00006D0032904D0FFDF03E4B0F8F41037E016 +:10BCD00090F87F10082936D0B0F89810B0F89A2064 +:10BCE00000248B1C9A4206D3511A891E0C04240C82 +:10BCF00001D0641EA4B290F8961039B190F87C205F +:10BD0000012309217030F5F73DF940B3FFF7BEFF7D +:10BD100078B129690020B1F89020B1F88E108B1C01 +:10BD20009A4203D3501A801E00D0401EA04200D277 +:10BD300084B20CB1641EA4B22869B0F8F410214496 +:10BD4000A0F8F0102DE5B0F898100329BDD330F815 +:10BD5000701F428D1144491CA0F8801021E5002479 +:10BD6000EAE770B50C4605464FF4087200212046FC +:10BD7000F2F78DFB258014E5F8F7A2B92DE9F04123 +:10BD80000D4607460721F8F791F8041E3CD094F8B9 +:10BD9000C8010026A8B16E70092028700BE0268427 +:10BDA00084F8C861D4F8CA016860D4F8CE01A860EC +:10BDB000B4F8D201A88194F8C8010028EFD12E71FF +:10BDC000AEE094F8D40190B394F8D4010D2813D0C8 +:10BDD0000E2801D0FFDFA3E02088F8F798F9074686 +:10BDE000F7F745FE78B96E700E20287094F8D601EA +:10BDF00028712088E88014E02088F8F788F9074641 +:10BE0000F7F735FE10B10020BDE8F0816E700D200F +:10BE1000287094F8D60128712088E88094F8DA0117 +:10BE2000287284F8D4613846F7F71BFE78E0FFE704 +:10BE300094F80A0230B16E701020287084F80A62FB +:10BE4000AF806DE094F8DC0190B16E700A2028702C +:10BE50002088A880D4F8E011C5F80610D4F8E411C1 +:10BE6000C5F80A10B4F8E801E88184F8DC6157E00D +:10BE700094F8040270B16E701A20287005E000BFBB +:10BE800084F80462D4F80602686094F8040200287A +:10BE9000F6D145E094F8EA0188B16E70152028705B +:10BEA00008E000BF84F8EA6104F5F6702B1D07C8AE +:10BEB00083E8070094F8EA010028F3D130E094F811 +:10BEC000F80170B16E701C20287084F8F861D4F805 +:10BED000FA016860D4F8FE01A860B4F80202A881F3 +:10BEE0001EE094F80C0238B11D20287084F80C6212 +:10BEF000D4F80E02686013E094F81202002883D090 +:10BF00006E701620287007E084F81262D4F81402CC +:10BF10006860B4F81802288194F812020028F3D15E +:10BF2000012071E735480021C16101620846704770 +:10BF300030B5324D0C46E860FFF7F4FF00B1FFDF8B +:10BF40002C7130BD002180F87C1080F87D1080F8C5 +:10BF5000801090F8FB1009B1022100E00321FEF7E8 +:10BF60003FBC2DE9F041254C0546206909B100216F +:10BF700004E0B0F80611B0F8F6201144A0F806115C +:10BF800090F88C1139B990F87F2001231946703050 +:10BF9000F4F7F8FF30B1206930F89C1FB0F85A2050 +:10BFA00011440180206990F8A23033B1B0F89E109E +:10BFB000B0F8F6201144A0F89E1090F9A670002F5A +:10BFC00006DDB0F8A410B0F8F6201144A0F8A410D3 +:10BFD00001213D2615B180F88D6017E02278022AF4 +:10BFE0000ED0012A15D0A2784AB380F88C1012F036 +:10BFF000140F11D01E2117E0FC6202000001002086 +:10C0000090F8E620062A3CD016223AE080F88C1000 +:10C0100044E090F88E2134E0110702D580F88D605D +:10C020003CE0910603D5232180F88D1036E090077F +:10C0300000D1FFDF21692A2081F88D002AE02BB191 +:10C04000B0F89E20B0F8A0309A4210D2002F05DD43 +:10C05000B0F8A420B0F8A0309A4208D2B0F89C30D2 +:10C06000B0F89A20934204D390F88C310BB122227D +:10C0700007E090F880303BB1B0F89830934209D394 +:10C08000082280F88D20C1E7B0F89820062A01D355 +:10C090003E22F6E7206990F88C1019B12069BDE8BE +:10C0A000F0414FE7BDE8F0410021FEF799BB2DE9D3 +:10C0B000F047FF4C81460D4620690088F8F739F8B3 +:10C0C000060000D1FFDFA0782843A070A0794FF0D0 +:10C0D00000058006206904D5A0F8985080F8045126 +:10C0E00003E030F8981F491C0180FFF7CFFD4FF0A7 +:10C0F000010830B3E088000506D5206990F8821069 +:10C1000011B1A0F88E501CE02069B0F88E10491CC7 +:10C1100089B2A0F88E10B0F890208A4201D3531A49 +:10C1200000E0002327897F1DBB4201D880F896805C +:10C13000914206D3A0F88E5080F80A822079E6F763 +:10C14000E3FEA0794FF0020710F0600F0ED02069D7 +:10C1500090F8801011B1032908D102E080F88080A6 +:10C1600001E080F880700121FEF73AFB206990F829 +:10C170008010012904D1E188C90501D580F88070BB +:10C18000B9F1000F72D1E188890502D5A0F81851E4 +:10C1900004E0B0F81811491CA0F8181100F035FBA4 +:10C1A000FEF719FDFFF72EFC2769B7F8F800401CD1 +:10C1B000A7F8F80097F8FC0028B100F01BFFA8B121 +:10C1C000A7F8F85012E000F012FF08B1A7F8F850F5 +:10C1D00000F015FF50B197F80401401CC0B287F879 +:10C1E0000401022802D927F8F85F3D732069012372 +:10C1F000002190F87D207030F4F7C4FE20B920694A +:10C2000090F87D000C2859D120690123002190F875 +:10C210007C207030F4F7B6FE48B32069012300217A +:10C2200090F87F207030F4F7ADFE00B3206990F8ED +:10C230008010022942D190F80401C0B93046F7F7C6 +:10C24000E6F9A0B1216991F8E400FE2836D1B1F8F1 +:10C25000F200012832D981F8FA80B1F89A00B1F8D9 +:10C260009820831E9A4203DB012004E032E025E09F +:10C27000801A401E80B2B1F8F82023899A4201D377 +:10C28000012202E09A1A521C92B2904200D9104642 +:10C29000012801D181F8FA5091F86F2092B98A6E85 +:10C2A00082B1B1F89420B1F87010511A09B2002986 +:10C2B00008DD884200DB084680B203E021690120E6 +:10C2C00081F8FA502169B1F870201044A1F8F40007 +:10C2D000FFF7EDFCE088C0F340214846FFF741FE40 +:10C2E000206980F8FB50BDE8F047FDF7FCB87049C5 +:10C2F00002468878CB78184312D10846006942B1CB +:10C300008979090703D590F87F00082808D0012013 +:10C310007047B0F84C10028E914201D8FEF7B1B9C7 +:10C320000020704770B5624C05460E46E0882843F1 +:10C33000E080A80703D5E80700D0FFDF6661EA07C1 +:10C340004FF000014FF001001AD0A661F278062AE2 +:10C3500002D00B2A14D10AE0226992F87D30172B03 +:10C360000ED10023E2E92E3302F8370C08E02269EF +:10C3700092F87D30112B03D182F8811082F8A80049 +:10C38000AA0718D56269D278052A02D00B2A12D1E1 +:10C390000AE0216991F87D20152A0CD10022E1E9FB +:10C3A000302201F83E0C06E0206990F87D20102A2A +:10C3B00001D180F88210280601D50820E07083E4BE +:10C3C0002DE9F84301273A4C002567F30701E58082 +:10C3D000A570E570257020618946804680F8FB7065 +:10C3E0000088F7F7A6FE00B9FFDF20690088FDF797 +:10C3F00042F820690088FDF764F82069B0F8F2106F +:10C4000071B190F8E410FE290FD190F88C1189B128 +:10C4100090F87F20012319467030F4F7B3FD78B10E +:10C42000206990F8E400FE2804D0206990F8E40028 +:10C43000FFF774FB206990F8FD1089B1258118E0A1 +:10C440002069A0F89C5090F88D1180F8E61000212A +:10C450000220FFF7CBF9206980F8FA500220E7E7C5 +:10C4600090F8C81119B9018C8288914200D881884E +:10C47000218130F8F61F491E8EB230F8021F314478 +:10C4800020F86019018831440180FFF7FFFB20B1DB +:10C49000206930F88E1F314401802069B0F8F21015 +:10C4A000012902D8491CA0F8F2102EB102E00000C8 +:10C4B0000001002080F8045180F8FA5090F87D10B7 +:10C4C0000B2901D00C2916D1B0F87020B0F8AA3190 +:10C4D000D21A12B2002A0EDBD0F8AC11816090F8AB +:10C4E000B01101730321F4F773F8206980F87D50CF +:10C4F00080F8B27026E0242910D1B0F87010B0F89E +:10C50000AA21891A09B2002908DB90F8C001FFF7B7 +:10C510004BF9206900F87D5F857613E090F87C1078 +:10C52000242901D025290DD1B0F87010B0F8AA0146 +:10C53000081A00B2002805DB0120FFF735F9206951 +:10C5400080F87C5020690146B0F8F6207030F4F78E +:10C55000B2FAFC480090FC4BFC4A4146484600F0C9 +:10C560007DFC216A11B16078FCF7B5FA20690123DE +:10C57000052190F87D207030F4F704FD002803D0E9 +:10C58000BDE8F84300F0FDB9BDE8F88300F015BD43 +:10C59000EF49C8617047EE48C069002800D001200B +:10C5A0007047EB4A50701162704710B5044600881E +:10C5B000A4F8CC01B4F8B001A4F8CE01B4F8B201EB +:10C5C000A4F8D001B4F8B401A4F8D201012084F891 +:10C5D000C801DF480079E6F797FC02212046F3F70F +:10C5E000F7FF002004F87D0F0320E07010BD401A13 +:10C5F00000B247F6FE71884201DC002801DC012010 +:10C6000070470020704710B5012808D0022808D0D4 +:10C61000042808D0082806D0FFDF204610BD0124DA +:10C62000FBE70224F9E70324F7E7C9480021006982 +:10C6300020F8A41F8178491C81707047C44800B558 +:10C64000016911F8A60F401E40B20870002800DAF8 +:10C65000FFDF00BDBE482721006980F87C10002163 +:10C6600080F8A011704710B5B94C206990F8A81156 +:10C67000042916D190F87C20012300217030F4F7B2 +:10C6800081FC00B9FFDF206990F8AA10890703D464 +:10C69000062180F87C1004E0002180F8A21080F8C8 +:10C6A000A811206990F87E00800707D5FFF7C6FF24 +:10C6B000206910F87E1F21F00201017010BDA4490D +:10C6C00010B5096991F87C200A2A09D191F8E22075 +:10C6D000824205D1002081F87C0081F8A20010BDC3 +:10C6E00091F87E20130706D522F0080081F87E001D +:10C6F000BDE81040A2E7FF2801D0FFDF10BDBDE874 +:10C700001040A7E7F8B5924C01230A21206990F860 +:10C710007C207030F4F736FC38B3A06901F07CFE61 +:10C72000C8B1A06901F072FE0746A06901F072FE6F +:10C730000646A06901F068FE0546A06901F068FEA2 +:10C7400001460097206933462A46303001F059FFF0 +:10C75000206901F082FF2169002081F8A20081F8A0 +:10C760007C00BDE8F840FEF7D2BBA07840F00100A5 +:10C77000A070F8BD10B5764C01230021206990F817 +:10C780007D207030F4F7FEFB30B1FFF74EFF2169DA +:10C79000102081F87D0010BD20690123052190F84B +:10C7A0007D207030F4F7EEFB08B1082000E0012096 +:10C7B000A07010BD70B5664C01230021206990F86F +:10C7C0007D207030F4F7DEFB012588B1A06901F00F +:10C7D000C4FD2169A1F8AA01B1F87010FFF707FFA5 +:10C7E00040B12069282180F88D1080F88C50E6E552 +:10C7F000A570E4E52169A06901F5D67101F0A8FDF5 +:10C8000021690B2081F87D00D9E510B5FEF779FF8D +:10C81000FEF760FE4E4CA079400708D5A07830B9ED +:10C82000206990F87F00072801D101202070FEF7D1 +:10C8300071FAA079C00609D5A07838B9206990F8B6 +:10C840007D100B2902D10C2180F87D10E0780007C3 +:10C850000ED520690123052190F87D207030F4F772 +:10C8600091FB30B10820A0702169002081F8D4012B +:10C8700010BDBDE81040002000F0C4BB10B5344C22 +:10C88000216991F87D2048B3102A06D0142A07D0D8 +:10C89000152A1AD01B2A2CD11AE001210B2019E0ED +:10C8A000FAF702FE0C2817D32069082100F58870DA +:10C8B000FAF7FEFD28B120690421DC30FAF7F8FD13 +:10C8C00000B9FFDF0121042004E000F017F803E0C5 +:10C8D00001210620FEF78AFF012010BD212A08D180 +:10C8E00091F8970038B991F8C00110B191F8C101E1 +:10C8F00008B1002010BD01211720EBE770B5144CE2 +:10C900000025206990F88F1101290AD002292ED123 +:10C9100090F8A810F1B1062180F8E610012102205C +:10C9200020E090F8D411002921D100F1C80300F5CE +:10C930008471002200F5C870F4F796FA01210520F1 +:10C9400010E00000AFC00100EFC2010025C30100EC +:10C950000001002090F8B000400701D5112000E050 +:10C960000D200121FEF742FF206980F88F5126E556 +:10C9700030B5FB4C05462078002818BFFFDFE57175 +:10C9800030BDF7490120887170472DE9F14FF54D11 +:10C990002846446804F1700794F86510608F94F895 +:10C9A0008280268F082978D0F4F797FBB8F1000F22 +:10C9B00004BF001D80B2864238BF304600F0FF0839 +:10C9C000DFF89C93E848C9F8240009F134006E6848 +:10C9D000406800F1700A90F882B096F86510358FC3 +:10C9E000708F08295DD0F4F778FB00BFBBF1000F12 +:10C9F00004BF001D80B2854238BF2846C0B29AF8F5 +:10CA00001210002918BF04210844C0B296F865101E +:10CA1000FBF735FCB87C002847D007F15801D24815 +:10CA200091E80E1000F5027585E80E10B96EC0F899 +:10CA30002112F96EC0F8251200F58170FBF7DBFFBB +:10CA4000C848007800280CBF0120002080F00101B8 +:10CA5000C6480176D7E91412C0E90412A0F5837222 +:10CA6000D9F82410FBF7F5F994F86500012808BF00 +:10CA700000220CD0022808BF012208D0042808BFD9 +:10CA8000032204D008281ABFFFDF002202224146F9 +:10CA90000120FBF7F9F90EE0FFE70421F4F71DFB95 +:10CAA00084E70421F4F719FBA0E7D9F82400FBF789 +:10CAB000A2FFFBF715FA009850B994F8650094F8B6 +:10CAC000661010F00C0F08BF00219620FBF7B4FF92 +:10CAD00094F8642001210020FCF76BF894F82C00F6 +:10CAE000012808BFFCF735F8022089F80000FCF7A0 +:10CAF0003FFC002818BFFFDFBDE8F88F2DE9F04F9D +:10CB0000DFF860A28BB050469AF800204068AAF186 +:10CB10001401059190F8751000F1700504464FF06E +:10CB200008080127AAF13406A1B3012900F0068103 +:10CB3000022900F00781032918BFFFDF00F01881E8 +:10CB4000306A0423017821F008010170AA7908EA0B +:10CB5000C202114321F004010170EA7903EA820262 +:10CB6000114321F01001017095F80590F06AF6F775 +:10CB70005EFD8046FCF7C9FCB9F1020F00F00081B0 +:10CB8000B9F1010F00F00081B9F1030F00F000814D +:10CB900000F003B9FFE795F80CC04FF002094FF021 +:10CBA000000BBCF1240F1CBF6B7B242B08D0BCF105 +:10CBB0001F0F18BFBCF1200F2AD0222B4DD077E0D9 +:10CBC00094F864109AB190F8AC01002874D0082948 +:10CBD00018BF042969D0082818BF042865D0012986 +:10CBE00018BF012853D000BF4FF0020164E090F855 +:10CBF0001201002860D0082918BF042955D0082840 +:10CC000018BF042851D0012918BF01283FD0EBE7F5 +:10CC1000222B22D0002A4BD090F8C20194F8641045 +:10CC200010F0040F18BF40460CD0082918BF042983 +:10CC30003BD0082818BF042837D0012918BF012885 +:10CC400025D0D1E710F0010F18BF3846EDD110F014 +:10CC5000020F18BF4846E8D12EE04AB390F8C2212F +:10CC600090F85D0094F8641002EA000010F0040FE0 +:10CC700018BF40460ED0082918BF042915D008282F +:10CC800018BF042811D0012918BF0128ACD14FF0DA +:10CC9000010111E010F0010F18BF3846EBD110F080 +:10CCA000020F18BF4846E6D106E04FF0080103E046 +:10CCB00094F864100429F8D0A08E11F00C0F18BF5E +:10CCC0004FF42960F4F709FA218E814238BF0846F3 +:10CCD000ADF80400A4F84C000598FCF7F5FB60B132 +:10CCE0007289316A42F48062728172694FF48060A5 +:10CCF000904703206871EF7022E709AA01A9F06A42 +:10CD0000F6F7CFFB306210B195F8371021B10598D6 +:10CD1000FCF7AEFB6F7113E79DF8241031B9A0F852 +:10CD200000B080F802B0012101F09EFABDF80410B5 +:10CD3000306A01F0C7FB85F8059001E70598FCF71C +:10CD400097FBFDE6B4F84C00ADF8040009AA01A970 +:10CD5000F06AF6F7A6FB3062002808BFFFDFEFE6B7 +:10CD60002401002058010020300D0020380F002041 +:10CD70000598FCF7A9FB002808BFFFDFE0E600BF2D +:10CD800030EA080009D106E030EA080005D102E0E7 +:10CD9000B8F1000F01D0012100E00021306A0278D3 +:10CDA00042EA01110170697C00291CBF69790129DF +:10CDB0003BD005F15801FD4891E80E1000F50278CE +:10CDC00088E80E10A96EC0F82112E96EC0F825128D +:10CDD00000F58170FBF70FFE9AF8000000280CBFE9 +:10CDE00001210021F2480176D5E91212C0E90412AE +:10CDF000A0F58371326AFBF72CF894F864000128DF +:10CE000008BF00220CD0022808BF012208D0042845 +:10CE100008BF032204D008281ABFFFDF0022022225 +:10CE2000FB210020FBF730F803E0FBF7E4FDFBF704 +:10CE300057F8012194F865200846FBF7BAFE3771D0 +:10CE4000306A0188F181807830743770FCF799FA84 +:10CE5000002818BFFFDF0BB0BDE8F08F2DE9F043CD +:10CE6000D44D87B081462878DDF838801E461746B5 +:10CE70000C4628B9002F1CBF002EB8F1000F00D1BE +:10CE8000FFDFC5F81C80C5E90D94C5E905764FF0B4 +:10CE90000000A8716871E870A8702871C64E68819A +:10CEA000A881307804F170072088F7F742F9E8622A +:10CEB0002088F7F72CF92863FBF705FA94F9670047 +:10CEC000FBF7DAFA04F11200FBF76CFD04F10E0037 +:10CED000FBF7D8FA307800280CBF03200120FBF7BD +:10CEE00087FDB64890E80E108DE80E10D0E90410CA +:10CEF000CDE90410307800280CBFB148B148049047 +:10CF00006846FBF763FDF87EFBF7C4FAFBF76AFDA2 +:10CF100094F86F0078B9A06E68B1B88C39888842EF +:10CF200009D1B4F86C1001220844B88494F86E005A +:10CF3000A16EF8F7D8FE3078002804BFFF2094F8DF +:10CF400064401AD094F8651097F81280258F608F8E +:10CF5000082926D0F4F7C1F8B8F1000F04BF001D6E +:10CF600080B2854238BF2846C0B2B97C002918BFBC +:10CF70000421084494F86540C0B22146FBF77FF9CC +:10CF80003078214688B10120FBF74BFB7068D0F860 +:10CF90000001FBF733FD0120FFF7F7FC07B0BDE808 +:10CFA000F0830421F4F799F8D6E70020FBF739FB6A +:10CFB000FFF7A4FD07B0BDE8F0837F4800B5017816 +:10CFC0003438007819B1022818BFFFDF00BD0128EE +:10CFD00018BFFFDF00BD774810B50078022818BFE2 +:10CFE000FFDFBDE8104000F070BA00F06EBA714883 +:10CFF000007970476F488089C0F3002070476D4802 +:10D00000C07870472DE9F04706006B48694D4068CD +:10D0100000F17004686A90F8019018BF012E03D1E6 +:10D02000296B07F0F1FF6870687800274FF001085E +:10D03000A0B101283CD0022860D003281CBFFFDF2C +:10D04000BDE8F087012E08BFBDE8F087286BF6F732 +:10D05000E3FCE879BDE8F047E5F756BF012E14D0B0 +:10D06000A86A002808BFFFDF2889C21CD5E909107B +:10D07000F1F7E3F9A86A686201224946286BF6F7DE +:10D0800047FB022E08BFBDE8F087D4E91401401C1D +:10D0900041F10001C4E91401E079012801D1E771EF +:10D0A00001E084F80780E879BDE8F047E5F72CBF98 +:10D0B000012E14D0A86A002808BFFFDF2889C21CEF +:10D0C000D5E90910F1F7B9F9A86A68620022494662 +:10D0D000286BF6F71DFB022E08BFBDE8F087D4E9E8 +:10D0E0001410491C40F10000C4E91410E079012833 +:10D0F0000CBFE77184F80780BDE8F087012E06D0E9 +:10D10000286BF6F789FC022E08BFBDE8F087D4E94A +:10D110001410491C40F10000C4E91410E079012802 +:10D12000BFD1BCE72DE9F041234D2846A5F13404D9 +:10D13000406800F170062078012818BFFFDFB07842 +:10D140000127002158B1B1706289042042F0040225 +:10D150006281626990472878002818BF3771216A78 +:10D160000322087832EA000009D1628912F4806F44 +:10D1700005D042F002026281626902209047A169F3 +:10D180000020884760B3607950BB287818B30E48F8 +:10D19000007810F0100F04D10449097811F0100F35 +:10D1A0001ED06189E1B9A16AA9B90FE0300D002054 +:10D1B000380F0020240100205801002004630200E1 +:10D1C000BB220200A7A8010032010020218911B171 +:10D1D00010F0100F04D0BDE8F0410020FFF7D5BBE0 +:10D1E000BDE8F04100F071B92DE9F05FCC4E044686 +:10D1F0003046A6F134054068002700F1700A28780F +:10D20000B846022818BFFFDFA889FF2240F400704B +:10D21000A881706890F864101046FBF730F89AF80F +:10D2200012004FF00109002C00F0F080FAF77DFEAB +:10D23000FAF76BFE90B99AF8120078B1686A4178F3 +:10D2400061B100789AF80710C0F3C000884205D198 +:10D2500085F80290BDE8F05F00F037B9686A417860 +:10D260002981002908BFAF6203D0286BF6F70AFABC +:10D27000A862A88940F02000A881EF70706800F1D2 +:10D28000700B044690F82C0001281BD1FBF757FCCB +:10D2900059462046F3F729FEA0B13078002870687F +:10D2A0000CBF00F59A7000F50170218841809BF851 +:10D2B000081001719BF80910417180F80090E8791D +:10D2C000E5F722FE686A9AF806100078C0F380003D +:10D2D00088423AD0706800F1700490F87500002818 +:10D2E0002FD002284AD06771307800281CBF2079DF +:10D2F000002809D027716A89394642F010026A81F4 +:10D300006A694FF010009047E078A0B1E770FCF731 +:10D31000EAF8002808BFFFDF08206A89002142F0F0 +:10D3200008026A816A699047D4E91210491C40F1E9 +:10D330000000C4E91210A07901280CBFA77184F87D +:10D340000690A88940F48070A881696A9AF807302D +:10D350000878C0F3C0029A424DD1726800F0030011 +:10D3600002F17004012818BF02282DD003281CBF29 +:10D37000687940F0040012D068713CE0E86AF6F782 +:10D38000BCF8002808BFFFDFD4E91210491C40F1A7 +:10D390000000C4E91210E879E5F7B6FDA3E784F8C8 +:10D3A0000290AA89484642F40062AA816A8942F042 +:10D3B00001026A816A699047E079012801D1E77129 +:10D3C00019E084F8079016E04878D8B1A98941F4AB +:10D3D0000061A981A96A71B1FB2884BF687940F016 +:10D3E0001000C9D8A879002808BFC84603D08020FB +:10D3F0006A69002190470120A9698847E0B36879EC +:10D40000A0B13AE0E0790128DBD1D8E7002818BFC5 +:10D41000FAF7C5FDA88940F04000A881E97801200D +:10D42000491CC9B2E97001292DD8E5E7307890B9D7 +:10D430003C48007810F0100F04D13B49097811F0F6 +:10D44000100F1AD06989B9B9A96A21B9298911B10E +:10D4500010F0100F11D0B8F1000F1CBF0120FFF722 +:10D46000D1FDFFF74BFBB8F1000F08BFBDE8F09FFF +:10D470000220BDE8F05FC5E5FFE7B8F1000F1CBF73 +:10D480000020FFF7BFFDBDE8F05F00F01EB870B5EB +:10D490000D4606462248224900784C6850B1FAF7FA +:10D4A000F7FD034694F8642029463046BDE87040F5 +:10D4B000FDF78BBAFAF7ECFD034694F86420294691 +:10D4C0003046BDE8704004F0FCBE154910B54C680C +:10D4D000FBF714FBFBF7F3FAFBF7BCF9FBF768FA71 +:10D4E000FAF7FEFC94F82C00012808BFFBF727FB95 +:10D4F00094F86F0038B9A06E28B1002294F86E003D +:10D500001146F8F7F0FB094C00216269A0899047A9 +:10D51000E2696179A07890470020207010BD00007A +:10D520005801002032010020300D0020240100208D +:10D530002DE9F047FA4F894680463D782C0014D0FB +:10D540000126012D11DB601EC4B207EBC40090F868 +:10D550005311414506D10622494600F5AA70F0F75D +:10D560003FFF28B1761CAE42EDDD1020BDE8F0870C +:10D570002046BDE8F087EA498A78824286BF08449F +:10D5800090F843010020704710B540F2D3120021FB +:10D59000E348F0F77CFF0822FF21E248F0F777FF2D +:10D5A000E1480021417081704FF46171818010BDAC +:10D5B0002DE9F0410E460546FFF7BAFFD84C10287A +:10D5C00016D004EBC00191F85A0110F0010F1CBFF6 +:10D5D0000120BDE8F081607808283CBF012081F877 +:10D5E0005A011CD26078401C60700120BDE8F081B7 +:10D5F0006078082813D222780127501C207004EB91 +:10D60000C2083068C8F85401B088A8F85801102A38 +:10D6100028BFFFDF88F8535188F85A71E2E70020ED +:10D62000BDE8F081C04988707047BF488078704776 +:10D630002DE9F041BA4D00272878401E44B2002C55 +:10D6400030DB00BF05EBC40090F85A0110F0010F69 +:10D6500024D06878E6B2401E687005EBC6083046F4 +:10D6600088F85A7100F0E8FA102817D12878401E7F +:10D67000C0B22870B04211D005EBC001D1F85301FF +:10D68000C8F85301D1F85701C8F85701287800F0BD +:10D69000D3FA10281CBF284480F80361601E44B2EE +:10D6A000002CCFDAA0488770BDE8F0819C498A78C9 +:10D6B000824286BF01EB0010C01C002070472DE99C +:10D6C000F0470127994690463D460026FFF730FF78 +:10D6D000102820D0924C04EBC00191F85A1101F0AF +:10D6E000010600F0A9FA102815D0B9F1000F18BFF3 +:10D6F00089F80000A17881420DD904EB001111F1E5 +:10D70000030F08D0204490F84B5190F83B010128BA +:10D710000CBF0127002748EA060047EA0501084038 +:10D72000BDE8F0872DE9F05F1F4690468946064622 +:10D73000FFF7FEFE7A4C054610282ED000F07CFA4A +:10D7400010281CBF1220BDE8F09FA07808283ED208 +:10D75000A6781022701CA07004EB061909F10300D2 +:10D760004146F3F768FB09F1830010223946F3F7CD +:10D7700062FB10213846F3F74BFB3444102184F848 +:10D7800043014046F3F744FB84F84B0184F803510E +:10D79000002084F83B01BDE8F09FA078082816D24D +:10D7A00025784FF0000A681C207004EBC50BD9F8EF +:10D7B0000000CBF85401B9F80400ABF85801102D63 +:10D7C00028BFFFDF8BF853618BF85AA1C0E7072011 +:10D7D000BDE8F09F2DE9F041514CA078401E45B2C4 +:10D7E000002DB8BFBDE8F081EAB2A078401EC1B2FA +:10D7F000A17054FA85F090F803618A423DD004EBA1 +:10D80000011004EB0213D0F803C0C3F803C0D0F832 +:10D8100007C0C3F807C0D0F80BC0C3F80BC0D0F8DE +:10D820000FC0C3F80FC0D0F883C0C3F883C0D0F8CE +:10D8300087C0C3F887C0D0F88BC0C3F88BC0D0F8BE +:10D840008F00C3F88F006318A01801EB410193F813 +:10D8500003C102EB420204EB410180F803C104EB77 +:10D860004202D1F80BC1C2F80BC1B1F80F11A2F8F6 +:10D870000F1193F83B1180F83B1104EBC60797F8A2 +:10D880005A0110F0010F1CD1304600F0D5F91028D4 +:10D8900017D12078401EC0B22070B04211D004EBE6 +:10D8A000C000D0F85311C7F85311D0F85701C7F88A +:10D8B0005701207800F0C0F910281CBF204480F8E0 +:10D8C0000361681E45B2002D8EDABDE8F08116496D +:10D8D0004870704714484078704738B14AF2B81120 +:10D8E000884203D810498880012070470020704783 +:10D8F0000D488088704710B5FFF71AFE102804D035 +:10D9000000F09AF9102818BF10BD082010BD044976 +:10D910008A78824286BF01EB001083300020704776 +:10D92000600F00206C01002060010020FE4B93F886 +:10D9300002C084459CBF00207047184490F8030142 +:10D9400003EBC00090F853310B70D0F85411116004 +:10D95000B0F85801908001207047F34A114491F8C3 +:10D960000321F2490A700268C1F8062080884881C4 +:10D97000704770B516460C460546FBF7D5F8FAF722 +:10D98000C4F9EA48407868B1E748817851B12A196A +:10D99000002E0CBF8330C01CFAF791F9FAF7D8F9C2 +:10D9A000012070BD002070BD10B5FAF7FFF9002806 +:10D9B00004BFFF2010BDBDE81040FAF71DBAFAF70A +:10D9C000F5B9D9498A7882429CBF00207047084443 +:10D9D00090F8030101EBC00090F85A0100F001003B +:10D9E00070472DE9F047D04D00273E4628780028A3 +:10D9F00086BF4FF01009DFF83883BDE8F087AC78B8 +:10DA000021000CD00122012909DB601EC4B22819B3 +:10DA100090F80331B34203D0521C8A42F5DD4C46E4 +:10DA2000A14286BF05EB0410C01C002005EBC60A0E +:10DA30009AF85A1111F0010F16D050B1102C04D0E1 +:10DA4000291991F83B11012903D01021F3F7E0F9CE +:10DA500050B108F8074038467B1C9AF853210AF564 +:10DA6000AA71DFB2FAF7B5FC701CC6B22878B042D2 +:10DA7000C5D8BDE8F0872DE9F041AB4C002635460E +:10DA8000A07800288CBFAA4FBDE8F0816119C0B210 +:10DA900091F80381A84286BF04EB0510C01C00204A +:10DAA00091F83B11012903D01021F3F7B1F958B1D6 +:10DAB00004EBC800BD5590F8532100F5AA7130461B +:10DAC000731CDEB2FAF785FC681CC5B2A078A842C8 +:10DAD000DCD8BDE8F0810144934810B500EB02109A +:10DAE0000A4601218330FAF7EAF8BDE81040FAF758 +:10DAF0002FB90A468D4910B5497841B18A4B9978BA +:10DB000029B10244D81CFAF7DAF8012010BD002030 +:10DB100010BD854A01EB410102EB41010268C1F8E9 +:10DB20000B218088A1F80F0170472DE9F0417E4D4F +:10DB300007460024A878002898BFBDE8F081C0B24D +:10DB4000A04217D905EB041010F1830612D0102162 +:10DB50003046F3F75DF968B904EB440005EB400883 +:10DB600008F20B113A463046FBF74EFDB8F80F01AC +:10DB7000A8F80F01601CC4B2A878A042DFD8BDE8A5 +:10DB8000F081014610226B48F3F755B96948704798 +:10DB900065498A78824203D90A1892F843210AB16A +:10DBA0000020704700EB400001EB400000F20B103A +:10DBB00070475D498A78824206D9084490F83B0153 +:10DBC000002804BF01207047002070472DE9F04174 +:10DBD0000E460746144606213046F3F719F9524D12 +:10DBE00098B1A97871B105F59D7011F0010F18BFBA +:10DBF00000F8014FA978490804D0447000F8024F9A +:10DC0000491EFAD10120BDE8F08138463146FFF7C0 +:10DC10008FFC10280CD000F00FF8102818BF08282F +:10DC200006D0284480F83B414FF00100BDE8F08168 +:10DC30004FF00000BDE8F0813B4B10B4844698786B +:10DC400001000ED0012201290BDB401EC0B21C18BE +:10DC500094F80341644504BF10BC7047521C8A42CB +:10DC6000F3DD10BC1020704770B52F4C01466218D0 +:10DC7000A078401EC0B2A07092F8035181423CD0FF +:10DC800004EB011304EB001C01EB4101DCF8036021 +:10DC9000C3F80360DCF80760C3F80760DCF80B60CA +:10DCA000C3F80B60DCF80F60C3F80F60DCF883602A +:10DCB000C3F88360DCF88760C3F88760DCF88B60AA +:10DCC000C3F88B60DCF88FC0C3F88FC0231800EB5B +:10DCD000400093F803C104EB400082F803C104EB59 +:10DCE0004101D0F80BC1C1F80BC1B0F80F01A1F888 +:10DCF0000F0193F83B0182F83B0104EBC50696F84F +:10DD00005A0110F0010F18BF70BD2846FFF794FFAD +:10DD1000102818BF70BD2078401EC0B22070A842E5 +:10DD200008BF70BD08E00000600F00206001002007 +:10DD30006C0100203311002004EBC000D0F8531117 +:10DD4000C6F85311D0F85701C6F857012078FFF7ED +:10DD500073FF10281CBF204480F8035170BD0000E1 +:10DD60004078704730B50546007801F00F0220F08A +:10DD70000F0010432870092912D2DFE801F00507CF +:10DD800005070509050B0F0006240BE00C2409E02C +:10DD9000222407E001240020E87003E00E2401E0C3 +:10DDA0000024FFDF6C7030BD007800F00F0070477A +:10DDB0000A68C0F803208988A0F807107047D0F8D7 +:10DDC00003200A60B0F80700888070470A68C0F82E +:10DDD00009208988A0F80D107047D0F809200A6042 +:10DDE000B0F80D00888070470278402322F040028E +:10DDF00003EA81111143017070470078C0F380106D +:10DE000070470278802322F0800203EAC111114397 +:10DE1000017070470078C009704770B514460E460F +:10DE200005461F2A88BFFFDF2246314605F109005B +:10DE3000F0F703FBA01D687070BD70B544780E4606 +:10DE40000546062C38BFFFDFA01F84B21F2C88BFF9 +:10DE50001F24224605F109013046F0F7EEFA20466C +:10DE600070BD70B514460E4605461F2A88BFFFDFF9 +:10DE70002246314605F10900F0F7DFFAA01D68706F +:10DE800070BD0968C0F80F1070470A88A0F8132009 +:10DE900089784175704790F8242001F01F0122F025 +:10DEA0001F02114380F824107047072988BF0721FB +:10DEB00090F82420E02322F0E00203EA411111430C +:10DEC00080F8241070471F3008F065B810B504467C +:10DED00000F000FB002818BF204410BDC17811F0ED +:10DEE0003F0F1BBF027912F0010F0022012211F037 +:10DEF0003F0F1BBF037913F0020F002301231A44C5 +:10DF000002EB4202530011F03F0F1BBF027912F0E7 +:10DF1000080F0022012203EB420311F03F0F1BBF49 +:10DF2000027912F0040F00220122134411F03F0F76 +:10DF30001BBF027912F0200F0022012202EBC20265 +:10DF400003EB420311F03F0F1BBF027912F0100FD9 +:10DF50000022012202EB42021A4411F03F0F1BBFC4 +:10DF6000007910F0400F00200120104410F0FF0055 +:10DF700014BF012100210844C0B2704770B5027877 +:10DF8000417802F00F02082A4DD2DFE802F00408BF +:10DF90000B4C4C4C0F14881F1F280AD943E00C2946 +:10DFA00007D040E0881F1F2803D93CE0881F1F28A6 +:10DFB00039D8012070BD4A1EFE2A34D88446C07864 +:10DFC00000258209032A09D000F03F04601C884222 +:10DFD00004D86046FFF782FFA04201D9284670BDF1 +:10DFE0009CF803004FF0010610F03F0F1EBF1CF11C +:10DFF0000400007810F0100F13D06446042160462E +:10E0000000F068FA002818BF14EB0000E6D0017891 +:10E0100001F03F012529E1D280780221B1EB501FA8 +:10E02000DCD3304670BD002070BD70B5017801258D +:10E0300001F00F01002404290AD007290DD0082976 +:10E040001CBF002070BD40780E2836D0204670BD21 +:10E050004078801F1F2830D9F8E7844640789CF824 +:10E0600003108A09032AF1D001F03F06711C814296 +:10E07000ECD86046FFF732FFB042E7D89CF80300C7 +:10E0800010F03F0F1EBF1CF10400007810F0100FBD +:10E0900013D066460421604600F01CFA002818BF21 +:10E0A00016EB0000D2D0017801F03F012529CDD236 +:10E0B00080780221B1EB501FC8D3284670BD10B440 +:10E0C000017801F00F01032920D0052921D14478DE +:10E0D000B0F81910B0F81BC0B0F81730827D222CB0 +:10E0E00017D1062915D3B1F5486F98BFBCF5FA7F53 +:10E0F0000FD272B1082A98BF8A420AD28B429CBFC3 +:10E10000B0F81D00B0F5486F03D805E040780C2842 +:10E1100002D010BC0020704710BC012070472DE9D0 +:10E12000F0411F4614460D00064608BFFFDF21469A +:10E13000304600F0CFF9040008BFFFDF30193A463F +:10E140002946BDE8F041F0F778B9C07800F03F000B +:10E150007047C02202EA8111C27802F03F021143E7 +:10E16000C1707047C07880097047C9B201F00102E0 +:10E17000C1F340031A4402EB4202C1F3800303EBF4 +:10E180004202C1F3C00302EB4302C1F3001303EBED +:10E1900043031A44C1F3401303EBC30302EB4302EE +:10E1A000C1F380131A4412F0FF0202D0521CD2B203 +:10E1B0000171C37802F03F0103F0C0031943C1703D +:10E1C000511C417070472DE9F0410546C078164654 +:10E1D00000F03F041019401C0F46FF2888BFFFDFE6 +:10E1E000281932463946001DF0F727F9A019401CBE +:10E1F0006870BDE8F081C178407801F03F01401AB5 +:10E20000401E80B2704710B590F803C00B460CF06A +:10E210003F0144780CF03F0CA4EB0C0CACF1010C6A +:10E220001FFA8CF4944288BF14462BB10844011D98 +:10E2300022461846F0F701F9204610BD4078704795 +:10E2400000B5027801F0030322F003021A430270C2 +:10E25000012914BF0229002104D0032916BFFFDFC2 +:10E26000012100BD417000BD00B5027801F003033B +:10E2700022F003021A430270012914BF022900216F +:10E2800004D0032916BFFFDF012100BD417000BD8E +:10E29000007800F003007047417841B1C078192838 +:10E2A00003D2BC4A105C884201D101207047002093 +:10E2B000704730B501240546C17019293CBFB548E7 +:10E2C000445C02D3FF2918BFFFDF6C7030BD70B50E +:10E2D00015460E4604461B2A88BFFFDF65702A4696 +:10E2E0003146E01CBDE87040F0F7A7B8B0F8070071 +:10E2F0007047B0F809007047C172090A017370478E +:10E30000B0F80B00704730B4B0F80720B0F809C07F +:10E31000B0F805300179941F40F67A45AC4298BFB9 +:10E32000BCF5FA7F0ED269B1082998BF914209D293 +:10E3300093429FBFB0F80B00B0F5486F012030BC8E +:10E3400098BF7047002030BC7047001D07F023BE07 +:10E35000021D0846114607F01EBEB0F809007047BE +:10E36000007970470A684260496881607047426876 +:10E370000A60806848607047098881817047808999 +:10E38000088070470A68C0F80E204968C0F812106B +:10E390007047D0F80E200A60D0F81200486070472D +:10E3A0000968C0F816107047D0F81600086070476A +:10E3B0000A68426049688160704742680A60806804 +:10E3C000486070470968C1607047C068086070475E +:10E3D000007970470A684260496881607047426806 +:10E3E0000A608068486070470171090A417170478E +:10E3F0008171090AC17170470172090A417270473F +:10E400008172090AC172704780887047C08870475E +:10E41000008970474089704701891B2924BF4189C1 +:10E42000B1F5A47F07D381881B2921BFC088B0F52F +:10E43000A47F01207047002070470A684260496845 +:10E440008160704742680A6080684860704701795F +:10E4500011F0070F1BBF407910F0070F00200120BB +:10E460007047017911F0070F1BBF407910F0070FBB +:10E470000020012070470171704700797047417199 +:10E480007047407970478171090AC1717047C0882F +:10E4900070470179407901F007023F498A5C012AFF +:10E4A00006D800F00700085C01289CBF01207047D7 +:10E4B00000207047017170470079704741717047C3 +:10E4C0004079704730B50C460546FB2988BFFFDF11 +:10E4D0006C7030BDC378024613F03F0008BF704730 +:10E4E0000520127903F03F0312F0010F37D0002905 +:10E4F00014BF0B20704700BF12F0020F32D0012969 +:10E5000014BF801D704700BF12F0040F2DD00229E8 +:10E5100014BF401C704700BF12F0080F28D0032919 +:10E5200014BF801C704700BF12F0100F23D00429C5 +:10E5300014BFC01C704700BF12F0200F1ED0052969 +:10E540001ABF1230C0B2704712F0400F19D006291E +:10E550001ABF401CC0B27047072918D114E0002927 +:10E56000CAD114E00129CFD111E00229D4D10EE0A3 +:10E570000329D9D10BE00429DED108E00529E3D134 +:10E5800005E00629E8D102E0834288BF70470020F9 +:10E5900070470000246302001C63020030B490F84E +:10E5A00064508C88B1F808C015F00C0F1BD000BF68 +:10E5B000B4F5296F98BF4FF4296490F8655015F0B1 +:10E5C0000C0F17D0BCF5296F98BF4FF4296C4A88FF +:10E5D000C988A0F84420A0F84810A0F84640A0F848 +:10E5E0004AC030BC7047002B1CBF157815F00C0FCB +:10E5F000DED1E2E7002B1CBF527812F00C0FE1D104 +:10E60000E5E7DDF800C08181C2810382A0F812C075 +:10E6100070471B2202838282C281828142800281F2 +:10E62000028042848284828359B14FF429614183FC +:10E63000C18241820182C18041818180C184018582 +:10E6400070474FF4A4714183C18241820182C1802D +:10E6500041818180C18401857047F0B4B0F84820C1 +:10E66000818F468EC58E8A4228BF0A4690F8651073 +:10E670004FF0000311F00C0F18BF4FF4296106D1C1 +:10E68000B0F84AC0B0F840108C4538BF61464286A9 +:10E69000C186048FB0F83AC0944238BF14468C4506 +:10E6A00038BF8C460487A0F83AC0B2420ABFA942DC +:10E6B0004FF0010C4FF0000C058EB0F84410C28FE3 +:10E6C000848E914228BF114690F8642012F00C0FFE +:10E6D00018BF4FF4296206D1B0F84660B0F8422066 +:10E6E000964238BF324690F85A60022E0AD0018610 +:10E6F0008286A9420ABFA2420120002040EA0C0003 +:10E70000F0BC70478D4238BF2946944238BF22463C +:10E7100080F85A30EBE7508088899080C889D08093 +:10E72000088A1081488A508101201070704730B4E7 +:10E7300002884A80B0F830C0A1F804C0838ECB8034 +:10E74000428E0A81C48E4C81B0F85650A54204BF57 +:10E75000B0F85240944208D1B0F858409C4202BFF1 +:10E76000B0F854306345002301D04FF001030B7320 +:10E7700000F13003A0F852201A464B89D3848B88CD +:10E780009384CA88A0F858204FF00100087030BC6C +:10E79000704730B404460A46088E91F864104FF46E +:10E7A000747311F00C0F1CBF03EB801080B21ED0ED +:10E7B000918E814238BF0846118F92F865C01CF0D7 +:10E7C0000C0F1CBF03EB811189B218D0538F8B4201 +:10E7D00038BF194692F866301CF00C0F08BF0023B2 +:10E7E000002C0CBF0122002230BCF2F798BC022999 +:10E7F00007BF80003C30C000703080B2D8E7BCF169 +:10E80000020F07BF89003C31C900703189B2DDE7D2 +:10E810002DE9F041044606F099FCC8B9FE4F78682E +:10E8200090F8221001260025012914D00178012931 +:10E830001BD090F8281001291CBF0020BDE8F081F2 +:10E84000657018212170D0F82A10616080F8285076 +:10E850000120BDE8F081657007212170416A616087 +:10E8600080F822500120BDE8F081657014212170EC +:10E87000811C2022201DEFF7E0FD257279680D70C4 +:10E8800081F82850E54882888284C26B527B80F8E8 +:10E89000262080F82260C86B0088F5F738FCF5F771 +:10E8A000E0F8D5E7DC4840680178002914BF80888B +:10E8B0004FF6FF70704730B5D74C83B00D462078C7 +:10E8C0007F2808BFFFDF94F900307F202070D4F844 +:10E8D00004C09CF85000062808BF002205D09CF810 +:10E8E000500008280CBF022201229CF85400CDE9F8 +:10E8F000000302929CF873309CF880200CF13201E6 +:10E90000284606F08FFC03B0BDE8304006F01FBE7D +:10E910002DE9F04106F05FFC002818BF06F0E4FB8B +:10E92000BD4C606800F1840290F87610895C80F834 +:10E930008010002003F07EF828B3FAF753F86068DF +:10E94000B74990F855000D5C2846F9F7A3FD6068BB +:10E950004FF0000680F8735090F8801011F00C0F03 +:10E960000CBF25200F20F9F76CFC606890F8801030 +:10E970000120F9F711FE606890F84010032918BFD4 +:10E9800002290FD103E0BDE8F04101F02FB990F862 +:10E9900076108430085C012804D101221146002041 +:10E9A000FAF707F9FAF7D5F8606890F88050012D6A +:10E9B00007BF0127032100270521A068FFF799F869 +:10E9C000616881F8520040B1002F18BF402521D066 +:10E9D000F9F787F92846FAF79DF86068806DFAF72D +:10E9E0000DF8606890F85410FF291CBF6D30FEF7D9 +:10E9F000B4FFFF21606880F8531080F8541080F84D +:10EA0000626080F8616080F87D60062180F85010B7 +:10EA1000BDE8F08115F00C0F14BF55255025D7E740 +:10EA200070B57D4C0646606800F150052046806850 +:10EA300041B1D0F80510C5F81D10B0F80900A5F8CF +:10EA4000210003E005F11D01FFF7B9F9A068FFF708 +:10EA5000D4F985F82400A0680021032E018002D09B +:10EA6000052E04D03DE00321FFF77CF939E00521B4 +:10EA7000FFF778F96068C06B00F10E01A068FFF73E +:10EA800000FA6068C06B00F11201A068FFF7FDF9A1 +:10EA9000D4E90110CA6B527D8275CA6BD28AC275E5 +:10EAA000120A0276CA6B52884276120A8276CA6BC2 +:10EAB0009288C276120A0277CA6BD2884277120A0B +:10EAC0008277C96B0831FFF7FEF96068C06B017E81 +:10EAD000A068FFF7E0F9606890F88610A068FFF77B +:10EAE000E4F905F11D01A068FFF770F995F824100D +:10EAF000A068FFF786F9606800F1320590F8316090 +:10EB000090F8511091B190F84010032906D190F877 +:10EB10003910002918BF90F8560001D190F8530021 +:10EB2000FFF736F800281CBF012605462946A068D5 +:10EB3000FFF73EF93146A068BDE87040FFF754B9D1 +:10EB40003549496881F84B00704770B5324D002453 +:10EB50000126A8606968A1F8814081F8834081F8A6 +:10EB6000506091F85020022A1FBF91F850100129DF +:10EB7000FFDF70BD06F0CDFA6868047080F82240AF +:10EB800080F8284090F8520030B1F9F7CDFFF9F73E +:10EB9000BCF8686880F852406868072180F84A40ED +:10EBA00080F8396080F8404080F8554080F84B404C +:10EBB00080F87D4080F8381070BD2DE9F041164C8A +:10EBC000054686B0606890F85000012818BF0228FA +:10EBD00005D003281EBF0C2006B0BDE8F081687A7E +:10EBE000022839D0F9F76FFB0220F9F701FF0D4930 +:10EBF00001F10C0090E80D108DE80D10D1E907012E +:10EC0000CDE904016846F9F7E1FE606890F94B0030 +:10EC1000F9F732FCA06807E07401002044110020DD +:10EC20004363020040630200F9F7E5FEFC48F9F790 +:10EC3000B9FEFC48F9F726FC606890F831103230D4 +:10EC4000F9F7A5FB0F210720F9F7BFFB606890F8E3 +:10EC50003900E0B1FEF70FFF6168287A01F1840204 +:10EC600081F87600287A805C81F880006868886581 +:10EC70002A68CA65687A68B1012824D00525022867 +:10EC800008BF81F850506FD0032878D080E0FEF79D +:10EC9000A8FEE1E7E44B91F83850002291F85500C6 +:10ECA000401CA3FB006C4FEA5C0CACEB8C0C60448A +:10ECB00081F8550025FA00F010F0010F03D1501C27 +:10ECC000C2B2032AEAD3002681F87D6091F8490098 +:10ECD000002804BF91F85100002841D0F7F744FA0A +:10ECE000074660683946406CF7F736FFDFF83C832B +:10ECF000054690FBF8F008FB105041423846F6F705 +:10ED00001AFF6168486495FBF8F08A6F10448867C1 +:10ED1000FEF7EEFD01466068826F914220D847649D +:10ED2000866790F8510000281CBF0120FEF7FDFE09 +:10ED30000121606890F84A20002A1CBF90F8492001 +:10ED4000002A0DD090F8313000F13202012B04D1AD +:10ED5000527902F0C002402A08D03230FAF78CFC17 +:10ED60006168042081F8500012E008E00125FEF7F8 +:10ED70000DFF61682A463231FAF746FCF0E7002AB7 +:10ED800018BFFFDF012000F089FF606880F8505055 +:10ED900006B00020BDE8F08170B5A54D686890F818 +:10EDA000501004292ED005291CBF0C2070BD90F8EE +:10EDB0007D100026002990F883104FEA511124D0CD +:10EDC000002908BF012407D0012908BF022403D06D +:10EDD000022914BF00240824C06D00281CBF002095 +:10EDE00000F05CFF6868806DF9F708FE686890F8CD +:10EDF0004010022943D0032904BF90F86C10012968 +:10EE000041D04DE0FFF784FD52E0002908BF012406 +:10EE100007D0012908BF022403D0022914BF00240F +:10EE20000824C06D00281CBF002000F037FF686870 +:10EE3000806DF9F7E3FD686890F84010022906D06C +:10EE4000032904BF90F86C10012904D010E090F859 +:10EE50006C1002290CD1224614F00C0F04D090F84B +:10EE60004C00012808BF042201210020F9F7A1FE6F +:10EE70006868072180F8804080F8616016E090F8AB +:10EE80006C1002290CD1224614F00C0F04D090F81B +:10EE90004C00012808BF042201210020F9F789FE57 +:10EEA0006868082180F8804080F8616080F8501020 +:10EEB000002070BD5E49002210F0010F496802D0A9 +:10EEC000012281F8842010F0080F03D0114408209B +:10EED00081F88400002070475549496881F848004E +:10EEE000704710B5524C636893F83030022B14BF52 +:10EEF000032B00280BD100291ABF02290120002072 +:10EF00001146FEF7F8FC08281CBF012010BD606800 +:10EF100090F83000002816BF022800200120BDE82C +:10EF20001040FAF731BB4248406890F830000028A2 +:10EF300016BF022800200120FAF726BB3C49496889 +:10EF400081F8300070473A49496881F84A007047B3 +:10EF500070B5374C616891F83000002816BF022860 +:10EF60000020012081F8310001F13201FAF7F6FAB0 +:10EF7000606890F83010022916BF03290121002192 +:10EF800080F8511090F8312000F132034FF0000565 +:10EF9000012A04BF5B7913F0C00F0AD000F13203DD +:10EFA000012A04D15A7902F0C002402A01D000227D +:10EFB00000E0012280F84920002A04BF002970BD2A +:10EFC0008567F7F7D1F86168486491F85100002827 +:10EFD0001CBF0020FEF7A9FD0026606890F84A10CB +:10EFE00000291ABF90F84910002970BD90F831200F +:10EFF00000F13201012A04D1497901F0C001402910 +:10F0000005D02946BDE870403230FAF735BBFEF72F +:10F01000BDFD61683246BDE870403231FAF7F4BA9E +:10F020004063020046630200ABAAAAAA40420F0056 +:10F030007401002070B5FF4D0C4600280CBF012361 +:10F040000023696881F8393081F842004FF00800E8 +:10F0500081F856000CD1002C1ABF022C0120002090 +:10F060001146FEF748FC6968082881F8560001D06F +:10F07000002070BD022C14BF032C1220F8D170BDEB +:10F08000002818BF112070470328EA4A526808BFB9 +:10F09000D16382F840000020704710B5E54C6068ED +:10F0A00090F8401003291CBF002180F8601001D0A7 +:10F0B000002010BD0123C16B1A460020F2F738F87A +:10F0C0006168CA6B526A904294BF0120002081F8A7 +:10F0D0006000EDE7D748416891F84000032804D06C +:10F0E000012818BF022807D004E091F84200012847 +:10F0F00008BF70470020704791F84100012814BFF5 +:10F1000003280120F6D1704770B5F9F7F7FCF9F73D +:10F11000D6FCF9F79FFBF9F74BFCC64C002560685D +:10F1200090F8520030B1F9F7FFFCF8F7EEFD606897 +:10F1300080F8525060680121A0F8815080F8835017 +:10F1400080F8501080F82850002070BDB94810B5E4 +:10F150004068643006F0B1FB002010BDB5480121C5 +:10F16000406890F84020032A03BF80F82A10C26B41 +:10F170001288002218BF80F82A20828580F8281083 +:10F180007047AC49496881F88600704701780023D0 +:10F1900011F0010FA749496809D04278032A08BF36 +:10F1A000CB6381F84020012281F884201346027845 +:10F1B00012F0040F0CD082784FF0000C032A08BF25 +:10F1C000C1F83CC081F840200B44082283F8842019 +:10F1D000C27881F830200279002A16BF022A012362 +:10F1E000002381F8393081F84120427981F83820B4 +:10F1F000807981F848004FF0000070478D484068E2 +:10F200008030704770B58B4C06460D46606890F8AC +:10F210005000032818BFFFDF022E1EBF032EFFDFA2 +:10F2200070BD002D18BF06F0A1F900216068A0F89C +:10F23000811080F88310012180F8501070BD00F01B +:10F24000D5BC2DE9F0477B4C0646894660684FF0F7 +:10F250000108072E90F8397038BF032540D3082ED7 +:10F2600084BF0020BDE8F08790F85010062908BF41 +:10F27000002105D090F8501008290CBF022101216F +:10F2800090F8800005F0AEFF002873D1A068C17827 +:10F2900011F03F0F12D0027912F0010F0ED0616809 +:10F2A0004FF0050591F85220002A18BFB9F1000F60 +:10F2B00016D091F88010012909D011E011F03F0F0C +:10F2C0001ABF007910F0100F002F53D14CE04FF00F +:10F2D00001024FF00501FEF74CFB616881F8520016 +:10F2E000A16808782944C0F3801030B1487900F053 +:10F2F000C000402808BF012000D00020616891F8BC +:10F300005210002918BF002807D0FEF74DFB014618 +:10F31000606880F8531080F86180606890F853103E +:10F32000FF292AD080F854100846FEF74AFB40EA2D +:10F330000705606890F85320FF2A18BF002D10D0F1 +:10F34000072E0ED3A068C17811F03F0F09D00179C4 +:10F3500011F0020F05D00B21FEF7BDFB606880F8AD +:10F3600062802846BDE8F087FEF75FF9002808BFF5 +:10F37000BDE8F0870120BDE8F087A36890F8392048 +:10F3800059191B78C3F3801C00F153036046FEF744 +:10F3900096F90546CDE72DE9F043264C87B0A068E5 +:10F3A000FEF7E0FE7F264FF00108002558B1022746 +:10F3B00001287DD0022800F0EF80F9F74BFA07B062 +:10F3C0000620BDE8F083F9F745FA616891F840003E +:10F3D000032800F01581A068C27812F03F0F05D015 +:10F3E000037913F0100F18BF012700D10027002F59 +:10F3F00014BF0823012312F03F0F00F001810079B0 +:10F4000033EA000240F0FC8010F0020F08D091F8BF +:10F410008000002105F064FE002808BF012000D014 +:10F4200000208DF80C508DF810508DF814504FF0CE +:10F43000FF0801E074010020D0B105AA03A904A8C7 +:10F4400000F07AFC606890F831809DF80C0000288C +:10F4500018BF48F002080BD1A068FEF7DBFC81461C +:10F460000121A068FEF732FD4946F8F79AFF28B35C +:10F47000FFB1012000F0DDFB002852D020787F286A +:10F4800008BFFFDF94F900102670606890F85420E0 +:10F49000CDE90021029590F8733090F8802000F1BA +:10F4A0003201404605F0BEFE606880F86C50A3E073 +:10F4B00038E041460020FFF7FEF9A1E0606890F8CF +:10F4C0004100032818BF02282BD19DF81000002806 +:10F4D00027D09DF80C00002823D1F7B1012000F0BF +:10F4E000A8FB00281DD020787F2808BFFFDF94F9F3 +:10F4F00000102670606890F85420CDE90021029534 +:10F5000090F8733090F8802000F13201FE2005F071 +:10F5100089FE606880F86C506EE0FE210020FFF7E5 +:10F52000CAF96DE0F9F796F9A0681821C27812F0CF +:10F530003F0F65D00279914362D10421FEF7C6FCEA +:10F54000616891F84020032A01BF8078B7EB501F13 +:10F5500091F86000002853D04FF0010000F069FBE3 +:10F56000E8B320787F2808BFFFDF94F900102670E9 +:10F57000606890F85420CDE90021029590F873302E +:10F5800090F8802000F13201FF2005F04BFE60680A +:10F5900080F86C8030E000BFF9F75CF9606890F8A3 +:10F5A000400003282CD0A0681821C27812F03F0F29 +:10F5B00026D0007931EA000022D1012000F039FB89 +:10F5C00068B120787F2808BFFFDF94F9001026700B +:10F5D000606890F85420CDE90021029500E00FE02A +:10F5E00090F8733090F8802000F13201FF2005F090 +:10F5F00019FE606880F86C7007B00320BDE8F083E6 +:10F6000007B00620BDE8F083F0B5FE4C074683B096 +:10F6100060686D460078002818BFFFDF002661682B +:10F620008E70C86B02888A8042884A8382888A8367 +:10F63000C088C88381F8206047B10121A068FEF727 +:10F6400045FC0546A0680078C10907E06946A06846 +:10F65000FEF7B5FBA0680078C0F380116068012751 +:10F6600090F85120002A18BF002904D06A7902F0CE +:10F67000C002402A26D090F84A20002A18BF00294C +:10F6800003D0697911F0C00F1CD000F10E00E3F730 +:10F69000B3FC616891F85400FF2819D001F1080209 +:10F6A000C91DFEF743F9002808BFFFDF6068C17974 +:10F6B00041F00201C171D0F86D104161B0F87110D4 +:10F6C00001830FE02968C0F80E10A9884182E0E7A5 +:10F6D000C86B427ECA71D0F81A208A60C08B8881BC +:10F6E0004E610E8360680770C26B90F84B1082F811 +:10F6F0006710C06B0088F4F70AFDF4F7A3F903B0B4 +:10F70000F0BD2DE9F041BF4C0546002760684FF081 +:10F7100001083E4690F84000012818BF022802D098 +:10F72000032818BFFFDF5DB1A068FEF727FC18B9FA +:10F73000A068FEF77AFC18B100F08FFB074645E0A1 +:10F74000606890F850007F25801F06283ED2DFE8D1 +:10F7500000F003191924352FAA48F9F709FA0028EF +:10F7600008BF2570F9F7EBF9606890F8520030B1E6 +:10F77000F9F7DAF9F8F7C9FA606880F85260F9F732 +:10F7800069F830E09F48F9F7F3F9002808BF2570C1 +:10F79000F9F7D5F905F0EAFEC3E09A48F9F7E8F978 +:10F7A000002808BF2570F9F7CAF9F9F753F81AE0ED +:10F7B0009448F9F7DDF930B9257004E09148F9F77C +:10F7C000D7F90028F8D0F9F7BAF9AAE0102F80F09D +:10F7D0003881DFE807F01E9DA6AAF1F108B3F2F127 +:10F7E000F1F10C832051BDE8F041FFF791B80320FF +:10F7F00002F020F9002870D000210320FFF710F953 +:10F80000012211461046F9F7D4F961680C2081F8FD +:10F810005000BDE8F081606800F15005042002F05E +:10F8200009F900287DD00E202870012002F0FDFC8F +:10F83000A06861680078C0F3401081F8750000216D +:10F840000520FFF7EDF87048A1684FF0200CC26B5F +:10F850000B78527B23F020030CEA42121A430A7001 +:10F86000C16B95F825304A7B1A404A73C06B28213A +:10F8700080F86610BDE8F081062002F0DBF8002871 +:10F880004FD0614D0F2085F85000022002F0CDFCD2 +:10F890006068012190F880200846F9F78AF9A0688D +:10F8A00061680078C0F3401081F8750001210520DF +:10F8B000FFF7B6F8E86B80F80D80A068017821F0BA +:10F8C00020010170F9F75DFD002818BFFFDF282037 +:10F8D000E96B81F86600BDE8F08122E0052002F0C6 +:10F8E000A9F8F0B101210320FFF79AF8F9F749FDD3 +:10F8F000002818BFFFDF6068012190F880200846CB +:10F90000F9F757F961680D2081F85000BDE8F081E2 +:10F910006068A0F8816080F8836080F85080BDE85E +:10F92000F081BDE8F04100F061B96168032081F821 +:10F930005000BDE8F041082002F077BC606890F804 +:10F940008310490908BF012507D0012908BF0225F6 +:10F9500003D0022914BF00250825C06D00281CBF54 +:10F96000002000F09BF96068806DF9F747F8606847 +:10F9700090F84010022906D0032904BF90F86C10BB +:10F98000012904D010E090F86C1002290CD12A460D +:10F9900015F00C0F04D090F84C00012808BF042289 +:10F9A00001210020F9F705F96068072180F88050EF +:10F9B00080F8616041E000E043E0606890F8831007 +:10F9C000490908BF012507D0012908BF022503D036 +:10F9D000022914BF00250825C06D00281CBF002087 +:10F9E00000F05CF96068806DF9F708F8606890F8DD +:10F9F000401002290AD0032904BF90F86C10012995 +:10FA000008D014E0740100204411002090F86C101C +:10FA100002290CD12A4615F00C0F04D090F84C00A6 +:10FA2000012808BF042201210020F9F7C2F860680C +:10FA3000082180F8805080F8616080F85010BDE89F +:10FA4000F081FFDFBDE8F08170B5FE4C606890F892 +:10FA5000503000210C2B38D001220D2B40D00E2B22 +:10FA600055D00F2B1CBFFFDF70BD042002F0DDFB63 +:10FA7000606890F880100E20F8F7E3FB606890F85B +:10FA8000800010F00C0F14BF282100219620F8F7F9 +:10FA9000D3FFF9F75EF86068052190F88050A06800 +:10FAA000FEF727F8616881F8520048B115F00C0F95 +:10FAB0000CBF50255525F8F714F92846F9F72AF810 +:10FAC00061680B2081F8500070BDF9F742F8002101 +:10FAD0009620F8F7B1FF6168092081F8500070BDE9 +:10FAE00090F88010FF20F8F7ACFB606890F8800079 +:10FAF00010F00C0F14BF282100219620F8F79CFF6E +:10FB0000F9F727F861680A2081F8500070BDA0F865 +:10FB1000811080F8831080F850200020FFF774FDDA +:10FB2000BDE87040032002F080BB70B5C54C606832 +:10FB300090F850007F25801F062828BF70BDDFE8A1 +:10FB400000F0171F1D032A11BE48F9F711F800280D +:10FB500008BF2570F8F7F3FFF8F77CFEBDE87040AA +:10FB6000FEF7D6BEB748F9F703F8C8B9257017E015 +:10FB7000B448F8F7FDFF40B9257006E005F0F6FC43 +:10FB8000B048F8F7F5FF0028F6D0F8F7D8FFBDE841 +:10FB9000704000F02BB8AB48F8F7EAFF0028E5D03A +:10FBA000F8F7CDFF60680021643005F037FEBDE84E +:10FBB000704000F01BB870B5A24C06460D460129F6 +:10FBC00008D0606890F880203046BDE87040134649 +:10FBD00002F077BBF8F75CFA61680346304691F8AB +:10FBE00080202946BDE8704002F06BBB70B5F8F785 +:10FBF00085FFF8F764FFF8F72DFEF8F7D9FE914C72 +:10FC00000025606890F8520030B1F8F78DFFF8F7E2 +:10FC10007CF8606880F852506068022180F85010CB +:10FC2000A0F8815080F88350BDE87040002002F0B9 +:10FC3000FCBA70B5834D06460421A868FEF746F964 +:10FC4000044605F0C8FA002808BF70BD207800F00F +:10FC50003F00252814D2F8F761FA217811F0800FBF +:10FC60000CBF1E214FF49671B4F80120C2F30C02B0 +:10FC700012FB01F10A1AB2F5877F28BF814201D237 +:10FC8000002070BD68682188A0F88110A17880F8F4 +:10FC900083103046BDE8704001F0CCBE2DE9F04144 +:10FCA000684C0746606800F1810690F883004009BF +:10FCB00008BF012507D0012808BF022503D002286C +:10FCC00014BF00250825F8F78DFE307800F03F06B8 +:10FCD0003046F8F7DFFB606880F8736090F86C00DE +:10FCE00002280CBF4020FF202946F8F7AAFA27B1C6 +:10FCF00029460120F8F795FC05E060682A46C16DA9 +:10FD00000120F8F7E2FCF8F724FF0521A068FDF7D1 +:10FD1000F0FE6168002881F8520008BFBDE8F0815C +:10FD200015F00C0F0CBF50245524F7F7DAFF2046CE +:10FD3000BDE8F041F8F7EEBE2DE9F74F414C002544 +:10FD4000914660688A4690F8510000280CBF4FF039 +:10FD500001084FF00008A0680178CE090121FEF7E4 +:10FD6000B5F836B1407900F0C000402808BF012640 +:10FD700000D00026606890F85210002961D090F8F9 +:10FD800040104FF0000B032906D190F839100029DC +:10FD900018BF90F856700ED1A068C17811F03F0FCF +:10FDA0001CBF007910F0010F02D105F061F940B3DA +:10FDB000606890F85370FF2F18BF082F21D0384685 +:10FDC000FDF7D9FB002818BF4FF00108002E38D0EE +:10FDD000606890F8620030B1FDF7F1FD054660689B +:10FDE00080F862B02DE03846FDF791FD054601210F +:10FDF000A068FEF76BF801462846F9F7D3FB0546E5 +:10FE00001FE0F6B1606890F86100D0B9A068C178D1 +:10FE100011F03F0F05D0017911F0010F18BF0B2130 +:10FE200000D105210022FDF7A4FD616881F8520090 +:10FE300038B1FDF7B9FDFF2803D06168012581F8CD +:10FE4000530001E0740100208AF800500098067009 +:10FE500089F8008003B0BDE8F08F2DE9F04FFF4C2A +:10FE600087B00025606890F850002E46801F4FF044 +:10FE70007F08062880F0D581DFE800F00308088BB2 +:10FE8000FDDB00F0F8FB054600F0CCB9F348F8F7CD +:10FE90006FFE002808BF84F80080F8F750FEA068C5 +:10FEA000FDF782FF0546072861D1A068FEF75AF9E1 +:10FEB0000146606890F86C208A4258D190F8501042 +:10FEC000062908BF002005D090F8500008280CBF74 +:10FED0000220012005F08AF970B90321A068FDF71E +:10FEE000F5FF002843D001884078C1F30B010009D9 +:10FEF00005F07BFC00283AD000212846FFF7A1F945 +:10FF0000A0B38DF80C608DF808608DF8046062680D +:10FF1000FF2592F8500008280CBF02210121A0689B +:10FF2000C37813F03F0F1CBF007910F0020F12D0FE +:10FF300092F8800005F0D4F868B901AA03A902A8D4 +:10FF4000FFF7FAFE606890F831509DF80C00002829 +:10FF500018BF45F002052B469DF804209DF80810B7 +:10FF60009DF80C0000F0D5F9054603E0FFE705F029 +:10FF7000FDFA0225606890F85200002800F05281D6 +:10FF8000F8F7D2FDF7F7C1FE606880F8526000F024 +:10FF900049B9A068FDF708FF0646A1686068CA78FD +:10FFA00090F86D309A4221D10A7990F86E309A42D9 +:10FFB0001CD14A7990F86F309A4217D18A7990F81B +:10FFC00070309A4212D1CA7990F871309A420DD1AC +:10FFD0000A7A90F872309A4208D1097890F8740041 +:10FFE000C1F38011814208BF012500D00025F8F738 +:10FFF00031FC9A48F8F7BCFD002808BF84F800805F +:020000040002F8 +:10000000F8F79DFD042E11D185B120787F2808BF17 +:10001000FFDF94F9003084F80080606890F8732066 +:1000200090F87D1090F8540005F06EFB062500F066 +:10003000F9B802278948F8F79BFD002808BF84F823 +:100040000080F8F77CFDA068FDF7AEFE0546A068CD +:10005000FEF788F8082D08BF00287CD1A0684FF073 +:100060000301C27812F03F0F75D0007931EA000029 +:1000700071D1606800E095E000F1500890F8390017 +:10008000002814BF98F8066098F803604FF0000944 +:1000900098F8020078B1FDF787FC0546FF280AD0E2 +:1000A0000146A068401DFDF758FCB5420CBF4FF05B +:1000B00001094FF000090021A068FDF707FF0622A3 +:1000C00008F11D01EEF78CF940B9A068FDF795FE27 +:1000D00098F82410884208BF012000D0002059EA77 +:1000E00000095DD0606800F1320590F831A098F801 +:1000F000010038B13046FDF74BFD00281CBF054616 +:100100004FF0010A4FF00008A06801784FEAD11BB8 +:100110000121FDF7DBFEBBF1000F07D0407900F0B5 +:10012000C000402808BF4FF0010B01D04FF0000B7A +:100130000121A068FDF7CAFE06222946EEF750F914 +:1001400030B9A068FDF766FE504508BF012501D013 +:100150004FF0000500E023E03BEA050018BFFF2E4A +:100160000DD03046FDF7D3FB060008D00121A06872 +:10017000FDF7ACFE01463046F9F714FA804645EA31 +:10018000080019EA000F0BD060680121643005F007 +:1001900045FB01273846FFF737FA052002F045F8FE +:1001A0003D463FE002252D48F8F7E2FC002808BF55 +:1001B00084F80080F8F7C3FCA068FDF7F5FD06465B +:1001C000A068FDF7CFFF072E08BF00282AD1A0683E +:1001D0004FF00101C27812F03F0F23D00279914312 +:1001E00020D1616801F150060021FDF76FFE062263 +:1001F00006F11D01EEF7F4F8A0B9A068FDF7FDFDCA +:1002000096F8241088420DD160680121643005F011 +:1002100005FBFF21022000F009F8002818BF032584 +:1002200000E0FFDF07B02846BDE8F08F2DE9F0437E +:100230000A4C0F4601466068002683B090F87D2086 +:10024000002A35D090F8500008280CBF022501255F +:10025000A168C87810F03F0F02E000007401002090 +:10026000FD484FF000084FF07F0990F900001CBFD7 +:10027000097911F0100F22D07F2808BFFFDF94F911 +:10028000001084F80090606890F85420CDE90021B7 +:10029000029590F8733090F8802000F132013846D2 +:1002A00004F0C0FF05F0AAFA10B305F050F92CE0F5 +:1002B000002914BF0221012180F87D10C2E77F28A8 +:1002C00008BFFFDF94F9001084F80090606890F890 +:1002D0005420CDE90021029590F8733090F88020E9 +:1002E00000F13201384604F09DFF05F030F90CE0D2 +:1002F0000220FFF79EFC30B16068012680F86C8018 +:10030000F8F7A8FA01E005F031F903B03046BDE88E +:10031000F0832DE9F047D04C054684B09A46174645 +:100320000E46A068FDF71EFF4FF00109002800F0FF +:10033000CF804FF00208012808D0022800F00E817B +:1003400005F014F904B04046BDE8F087A068092123 +:10035000C27812F03F0F00F059810279914340F0CA +:100360005581616891F84010032906D012F0020F00 +:1003700008BFFF2118D05DB115E00021FDF7A6FDF3 +:1003800061680622C96B1A31EEF72AF848BB1EE0F5 +:10039000FDF740FD05460121A068FDF797FD2946C0 +:1003A000F7F7FFFF18B15146012000F051B960681E +:1003B00090F84100032818BF022840F02781002E42 +:1003C0001CBFFE21012040F0438100F01FB9A0684E +:1003D000FDF713FD6168C96B497E884208BF01269D +:1003E00000D00026A068C17811F03F0F05D0017938 +:1003F00011F0020F01D06DB338E0616891F842202E +:10040000012A01D096B11BE0D6B90021FDF75EFDAF +:1004100061680268C96BC1F81A208088C883A06827 +:10042000FDF7EBFC6168C96B487609E091F8530071 +:1004300091F85610884203D004B04046BDE8F087DA +:100440006068643005F02EFA002840D004B00F2018 +:10045000BDE8F08767B1FDF7DDFC05460121A06826 +:10046000FDF734FD2946F7F79CFF08B1012200E0B3 +:100470000022616891F84200012807D040B92EB9E6 +:1004800091F8533091F856108B4201D1012100E0D0 +:1004900000210A421BD0012808BF002E11D14FF0C5 +:1004A0000001A068FDF712FD61680268C96BC1F820 +:1004B0001A208088C883A068FDF79FFC6168C96B1B +:1004C00048766068643005F0EDF90028BED19DE003 +:1004D00060682F46554690F840104FF002080329F7 +:1004E000AAD0A168CA7812F03F0F1BBF097911F09A +:1004F000020F002201224FF0FF0A90F85010082945 +:100500000CBF0221012192B190F8800004F0E8FDB7 +:1005100068B95FB9A068FDF77DFC07460121A068B6 +:10052000FDF7D4FC3946F7F73CFF48B1AA465146DF +:100530000020FFF77BFE002818BF4FF003087BE781 +:10054000606890F84100032818BF02287FF474AF58 +:10055000002E18BF4FF0FE0AE9D16DE7616891F8EF +:100560004030032B52D0A0684FF0090CC27812F033 +:100570003F0F4BD002793CEA020C47D1022B06D048 +:1005800012F0020F08BFFF2161D0E5B35EE012F068 +:10059000020F4FF07F0801D04DB114E001F164006B +:1005A00005F080F980B320787F2842D013E067B34C +:1005B000FDF730FC05460121A068FDF787FC2946C0 +:1005C000F7F7EFFE08B36068643005F06BF9D8B157 +:1005D00020787F282DD094F9001084F8008060687E +:1005E00090F85420CDE90021CDF8089090F87330B0 +:1005F00090F8802000F13201504604F013FE0D20E7 +:1006000004B0BDE8F08716E000E001E00220F7E763 +:10061000606890F84100032818BF0228F6D1002E28 +:10062000F4D04FF0FE014FF00200FEF744F9022033 +:10063000E6E7FFDFCFE7FDF7EDFB05460121A06808 +:10064000FDF744FC2946F7F7ACFE38B151460220CD +:10065000FEF731F9DAE7000074010020606890F8D5 +:100660004100032818BF0228D0D1002E1CBFFE2154 +:100670000220EDD1CAE72DE9F84F4FF00008F74806 +:10068000F8F776FA7F27F54C002808BF2770F8F7AF +:1006900056FAA068FDF788FB81460121FEF7D1FDDF +:1006A000616891F88020012A14D0042A1CBF082A0E +:1006B000FFDF00F0D781606890F8520038B1F8F79A +:1006C00033FAF7F722FB6168002081F852004046B8 +:1006D000BDE8F88F0125E24EB9F1080F3AD2DFE804 +:1006E00009F03EC00439393914FC0546F8F7B2F870 +:1006F000002D72D0606890F84000012818BF0228D1 +:100700006BD120787F2869D122E018B391F840009E +:10071000022802D0012818D01CE020787F2808BFCA +:10072000FFDF94F90000277000906068FF2190F8C7 +:10073000733090F85420323004F02FFF61680020AD +:100740004FF00C0881F87D00B5E720787F2860D154 +:10075000FFDF5EE0F8F77EF84FF00608ABE74FF0FA +:100760000008002800F0508191F84000022836D09F +:1007700001284BD003289ED1A068CA6BC37892F899 +:100780001AC0634521D1037992F81BC063451CD17F +:10079000437992F81CC0634517D1837992F81DC044 +:1007A000634512D1C37992F81EC063450DD1037A17 +:1007B00092F81FC0634508D1037892F819C0C3F3BB +:1007C0008013634508BF012300D0002391F8421035 +:1007D00001292CD0C3B300F013B93FE019E0207811 +:1007E0007F2808BFFFDF94F9000027700090606841 +:1007F000FF2190F8733090F85420323004F0CDFE91 +:1008000060684FF00C0880F87D5054E720787F280E +:100810009ED094F90000277000906068FF2190F846 +:10082000733090F85420323004F0B7FE16E0002BFD +:100830007ED102F11A01FDF7C2FAA068FDF7DDFAD8 +:100840006168C96B4876DBE0FFE796F85600082838 +:1008500070D096F8531081426AD0D5E04FF0060868 +:1008600029E7054691F8510000280CBF4FF0010B15 +:100870004FF0000B4FF00008A06810F8092BD209C8 +:1008800007D0407900F0C000402808BF4FF0010AAF +:1008900001D04FF0000A91F84000032806D191F8EA +:1008A0003900002818BF91F8569001D191F8539063 +:1008B0004846FDF72CF80090D8B34846FCF75BFE9D +:1008C000002818BF4FF0010BBAF1000F37D0A06815 +:1008D000A14600F10901009800E0B6E0F8F762FED9 +:1008E0005FEA0008D9F8040090F8319018BF49F089 +:1008F0000209606890F84010032924D0F7F7AAFF96 +:10090000002DABD0F7F75DFD002808BFB8F1000F50 +:100910007DD020787F2808BFFFDF94F90000277082 +:1009200000906068494690F8733090F8542002E0D7 +:1009300066E004E068E0323004F02FFE8EE7606885 +:1009400090F83190D5E7A168C06BCA78837E9A424F +:100950001BD10A79C37E9A4217D14A79037F9A4202 +:1009600013D18A79437F9A420FD1CA79837F9A4201 +:100970000BD10A7AC37F9A4207D10978407EC1F32E +:100980008011814208BF012700D0002796F853004C +:10099000082806D096F85610884208BF4FF0010983 +:1009A00001D04FF00009B8F1000F05D1BBF1000FE5 +:1009B00004D0F7F706FD08B1012000E000204DB19A +:1009C00096F84210012903D021B957EA090101D054 +:1009D000012100E00021084216D0606890F8421022 +:1009E000012908BF002F0BD1C06B00F11A01A068CC +:1009F000FDF7E5F9A068FDF700FA6168C96B487674 +:100A00004FF00E0857E602E0F7F724FF26E760688C +:100A100090F84100032818BF02287FF41FAFBAF1F5 +:100A2000000F3FF41BAF20787F2808BFFFDF94F949 +:100A30000000277000906068FE2190F8733090F8F5 +:100A40005420323004F0A9FD08E791F8481000293D +:100A500018BF00283FF47EAE0BE0000074010020B8 +:100A600044110020B9F1070F7FF474AE00283FF461 +:100A700071AEFEF790FC80461DE60000D0F8001134 +:100A800049B1D0E941231A448B691A448A61D0E9FB +:100A90003F12D16003E0FE4AD0F8FC101162D0E9A9 +:100AA0003F1009B1086170470028FCD00021816126 +:100AB00070472DE9FF4F06460C46488883B040F248 +:100AC000E24148430190E08A002500FB01FA94F8D6 +:100AD0007C0090460D2822D00C2820D024281ED03F +:100AE00094F87D0024281AD000208346069818B177 +:100AF0000121204603F0C0F894F8641094F86500D2 +:100B0000009094F8F0200F464FF47A794AB1012A08 +:100B100061D0022A44D0032A5DD0FFDFB5E0012076 +:100B2000E3E7B8F1000F00D1FFDFD94814F8641FE4 +:100B3000243090F83400F0F7C4FC01902078F8F7E6 +:100B4000CFFB4D4600F2E730B0FBF5F1DFF8409304 +:100B5000D9F80C0001EB00082078F8F7C1FB01463A +:100B600014F86409022816D0012816D040F6340083 +:100B700008444AF2EF010844B0FBF5F10198D9F8B6 +:100B80001C20411A514402EB08000D18012084F882 +:100B9000F0002D1D78E02846EAE74FF4C860E7E74B +:100BA000DFF8EC92A8F10100D9F80810014300D158 +:100BB000FFDFB848B8F1000F016801EB0A0506D065 +:100BC000D9F8080000F22630A84200D9FFDF032040 +:100BD00084F8F00058E094F87C20019D242A05D088 +:100BE00094F87D30242B01D0252A3AD1B4F8702016 +:100BF000B4F81031D21A521C12B2002A31DB94F828 +:100C0000122172B3174694F8132102B110460090D6 +:100C1000022916D0012916D040F6340049F60852B0 +:100C20008118022F12D0012F12D040F63400104448 +:100C3000814210D9081A00F5FA70B0FBF9F00544AA +:100C40000FE04846EAE74FF4C860E7E74846EEE7BA +:100C50004FF4C860EBE7401A00F5FA70B0FBF9F00A +:100C60002D1AB8F1000F0FD0DFF82482D8F8080051 +:100C700018B9B8F8020000B1FFDFD8F8080000F298 +:100C80002630A84200D9FFDF05B9FFDF2946D4F896 +:100C9000F400F4F750FFC4F8F400B06000203070A6 +:100CA0004FF0010886F80480204603F040F8ABF1CD +:100CB0000101084202D186F8058005E094F8F000B1 +:100CC000012844D003207071606A3946009A01F00F +:100CD00042FBF060069830EA0B0035D029463046DA +:100CE000F0F7BAF987B2204603F021F8B8420FD8DE +:100CF000074686F8058005FB07F1D4F8F400F4F701 +:100D00001AFFB06029463046F0F7A6F9384487B29A +:100D10003946204602F0B0FFB068C4F8F400A06E77 +:100D2000002811D0B4F87000B4F89420801A01B2F1 +:100D3000002909DD34F86C0F0144491E91FBF0F1E4 +:100D400089B201FB0020208507B0BDE8F08F0220AA +:100D5000B9E72DE9F04106460C46012001F0DBFA27 +:100D6000C5B20B2001F0D7FAC0B2854200D0FFDF38 +:100D70000025082C7ED2DFE804F00461696965C6AD +:100D80008293304601F0DDFA0621F3F78FF8040074 +:100D900000D1FFDF304601F0D4FA2188884200D02C +:100DA000FFDF94F8F00000B9FFDF204602F00FFEED +:100DB000374E21460020B5607580F561FDF7E9FCEE +:100DC00000F19807606AB84217D994F86500F7F700 +:100DD0000BF9014694F864004FF47A72022828D087 +:100DE000012828D040F6340008444AF2473108442C +:100DF000B0FBF2F1606A0844C51B21460020356152 +:100E0000FDF7C7FC618840F2E24251439830081A6E +:100E1000A0F22630706194F8652094F86410606A3E +:100E200001F099FAA0F5CB70B061BDE8F041F5F79B +:100E300060BE1046D8E74FF4C860D5E7BDE8F04182 +:100E400002F02FBEBDE8F041F7F7D5BE304601F005 +:100E500078FA0621F3F72AF8040000D1FFDF3046C4 +:100E600001F06FFA2188884200D0FFDF01220021C3 +:100E7000204600E047E0BDE8F04101F089BAF7F70D +:100E800073FDF7F7B8FE02204FF0E02104E0000008 +:100E9000CC11002084010020C1F88002BDE8F0815F +:100EA000304601F04EFA0621F3F700F8040000D1B5 +:100EB000FFDF304601F045FA2188884200D0FFDF8D +:100EC00094F8F000042800D0FFDF84F8F05094F884 +:100ED000FA504FF6FF76202D00D3FFDFFA4820F8B6 +:100EE000156094F8FA00F5F720F900B9FFDF20202B +:100EF00084F8FA002046FFF7C1FDF4480078BDE809 +:100F0000F041E2F701B8FFDFC8E770B5EE4C00250D +:100F1000443C84F82850E07868B1E570FEF71EF98B +:100F20002078042803D0606AFFF7A8FD6562E748CF +:100F30000078E1F7E9FFBDE8704001F03ABA70B51A +:100F4000E14C0146443CE069F5F706FE6568A2788D +:100F500090FBF5F172B140F27122B5FBF2F292B260 +:100F6000A36B01FB02F6B34202D901FB123200E08F +:100F70000022A2634D43002800DAFFDF2946E06922 +:100F8000F4F7D9FDE06170BD2DE9F05FFEF736F9A9 +:100F90008246CD48683800F1240881684646D8F872 +:100FA0001800F4F7C8FD0146F069F5F7D5FD4FF0DC +:100FB0000009074686F835903C4640F28F254E469C +:100FC0001EE000BF0AEB06000079F7F70DF80146B6 +:100FD0004AF2B12001444FF47A70B1FBF0F008EB13 +:100FE0008602414692681044844207D3241A91F83D +:100FF0003500A4F28F24401C88F83500761CF6B228 +:1010000098F83600B042DDD8002C10DD98F8351085 +:10101000404608EB81018968A14208D24168C91B9A +:10102000B1F5247F00D30D466C4288F8359098F8CE +:101030003560C3460AEB060898F80400F6F7D4FFBB +:101040004AF2B12101444FF47A7AB1FBFAF298F8EE +:101050000410082909D0042909D0002013180429F4 +:101060000AD0082908D0252207E0082000E0022045 +:1010700000EB40002830F1E70F22521D4FF4A8701A +:10108000082914D0042915D0022916D04FF0080CD5 +:101090005FF0280012FB0C00184462190BEB86036A +:1010A00010449A68D84690420BD8791925E04FF041 +:1010B000400CEFE74FF0100CECE74FF0040C182059 +:1010C000E8E798F8352098F836604046B24210D2EA +:1010D000521C88F835203C1B986862198418084611 +:1010E000F6F782FF4AF2B1210144B1FBFAF001198F +:1010F00003E080F83590D8F80410D8F81C00BDE85B +:10110000F05FF4F718BD2DE9FE4F14460546FEF7D3 +:1011100075F8DFF8B4A10290AAF1440A50469AF893 +:1011200035604FF0000B0AEB86018968CAF83C1065 +:10113000F4B3044600780027042825D005283ED0C3 +:10114000FFDFA04639466069F4F7F5FC0746F5F77E +:101150000BF881463946D8F80440F5F7FDFC401EEF +:1011600090FBF4F0C14361433846F4F7E4FC0146D8 +:10117000C8F81C004846F5F7EFFC002800DDFFDF4B +:10118000012188F813108DE0D4F81490D4F804806D +:1011900001F07AF9070010D0387800B9FFDF7969DB +:1011A00078684A460844414601F05AF9074600E08B +:1011B0000BE04045C5D9FFDFC3E75F46C1E7606A82 +:1011C00001F004F940F6B837BBE7C1690AEB460005 +:1011D0000191408D10B35446DAF81400FFF7AFFECA +:1011E0006168E069F4F7A7FC074684F835B0019C14 +:1011F000D0462046DAF81410F5F7AEFC81463946A1 +:101200002046F5F7A9FCD8F804200146B9FBF2F016 +:10121000B1FBF2F1884242D0012041E0F4F7A4FF93 +:10122000FFF78DFEFFF7B0FE9AF83510DAF804905C +:101230000AEB81010746896800913946DAF81C00FB +:10124000F5F78AFC00248046484504DB98FBF9F456 +:1012500004FB09F41AE0002052469AF8351007E022 +:1012600002EB800304F28F249B68401C1C44C0B234 +:101270008142F5D851B10120F6F7B6FE4AF2B1210C +:1012800001444FF47A70B1FBF0F004440099A8EBEC +:1012900004000C1A00D5FFDFCAF83C40A7E7002085 +:1012A00088F813009AF802005446B8B13946E0694C +:1012B000F5F752FC0146A26B40F2712042438A428C +:1012C00006D2C4F83CB009E03412002080010020AE +:1012D000E06B511A884200D30846E063AF6085F89E +:1012E00000B001202871029F94F835003F1DC05DB9 +:1012F000F6F77AFE4AF23B5101444FF47A70B1FBA3 +:10130000F0F0E16BFE300844E8602078042808D152 +:1013100094F8350004EB4000408D0A2801D20320E8 +:1013200000E00220687104EB4600408DC0B1284601 +:101330006168EFF791FE82B20020761C0CE000BFDE +:1013400004EB4001B0424B8D13449BB24B8501D35B +:101350005B1C4B85401CC0B294F836108142EFD222 +:10136000A8686061A06194F8350004EB4001488DE5 +:10137000401C488594F83500C05D082803D0042837 +:1013800003D000210BE0082100E0022101EB410124 +:1013900028314FF4A872082804D0042802D002286B +:1013A00007D028220A44042805D0082803D0252184 +:1013B00002E01822F6E70F21491D08280CD0042866 +:1013C0000CD002280CD0082011FB0020E16B8842D1 +:1013D00008D20120BDE8FE8F4020F5E71020F3E79A +:1013E0000420F1E70020F5E770B5FE4C061D14F867 +:1013F000352F905DF6F7F8FD4FF47A7100F2E73083 +:10140000B0FBF1F0D4F8071045182078805DF6F7AE +:1014100073FE2178895D082903D0042903D00022B6 +:101420000BE0082200E0022202EB420228324FF4D5 +:10143000A873082904D0042902D0022907D0282340 +:101440001344042905D0082903D0252202E01823DB +:10145000F6E70F22521D08290AD004290AD00229D2 +:101460000AD0082112FB0131081A281A293070BD50 +:101470004021F7E71021F5E70421F3E72DE9FF41CB +:1014800007460C46012000F046FFC5B20B2000F0D5 +:1014900042FFC0B2854200D0FFDF20460126002572 +:1014A000D04C082869D2DFE800F004304646426894 +:1014B0006865667426746078002819D1FDF79EFE71 +:1014C000009594F835108DF808104188C90411D0A2 +:1014D000206C019003208DF80900C24824388560F3 +:1014E000C56125746846FDF768FB002800D0FFDF62 +:1014F000BDE8FF81FFF778FF0190E07C10B18DF827 +:101500000950EAE78DF80960E7E7607840B1207C90 +:1015100008B9FDF7F9FD6574BDE8FF41F4F72FBD8B +:10152000A674FDF739FC0028E2D0FFDFE0E7BDE854 +:10153000FF41F7F760BBFDF761FE4088C00407D0AC +:1015400001210320FDF75EFEA7480078E1F7DCFCEF +:10155000002239466846FFF7D6FD38B1694638465D +:1015600000F0EDFE0028C3D1FFDFC1E7E670FFF712 +:10157000CCFCBDE7BDE8FF41C7E4FFDFB8E7994910 +:1015800050B101228A704A6840F27123B2FBF3F233 +:1015900002EB0010886370470020887070472DE9C7 +:1015A000F05F894640F271218E4E48430025044683 +:1015B000706090462F46D0074AF2B12A4FF47A7BEA +:1015C0000FD0B9F800004843B0600120F6F70CFDD9 +:1015D00000EB0A01B1FBFBF0241AB7680125A4F265 +:1015E0008F245FEA087016D539F8151040F2712083 +:1015F000414306EB85080820C8F80810F6F7F4FC0C +:1016000000EB0A01B1FBFBF0241AD8F80800A4F2A1 +:101610008F2407446D1CA74219D9002D17D0391B00 +:10162000B1FBF5F0B268101AB1FBF5F205FB12122E +:10163000801AB060012008E0B1FBF5F306EB8002F0 +:101640009468E31A401CC0B29360A842F4D3BDE88A +:10165000F09F2DE9F041634C00262078042804D047 +:101660002078052801D00C2018E401206070607CEF +:10167000002538B1EFF3108010F0010F72B610D0D2 +:1016800001270FE0FDF7BAFD074694F82000F5F7B3 +:10169000B2F87888C00411D000210320FDF7B2FD14 +:1016A0000CE00027607C38B1A07C28B1FDF72CFD50 +:1016B0006574A574F4F763FC07B962B694F820006A +:1016C000F5F705FB94F8280030B184F8285020780D +:1016D000052800D0FFDF0C26657000F06AFE30465A +:1016E00012E4404810B5007808B1FFF7B2FF00F0EF +:1016F000D4FE3C4900202439086210BD10B53A4C94 +:1017000058B1012807D0FFDFA06841F66A0188427E +:1017100000D3FFDF10BD40F6C410A060F4E73249EB +:1017200008B508702F4900200870487081F828001B +:10173000C8700874487488742022486281F8202098 +:10174000243948704FF6FF7211F1680121F810201A +:10175000401CC0B22028F9D30020FFF7CFFFFFF7CD +:10176000C0FF1020ADF80000012269460420FFF7F9 +:1017700016FF08BD7FB51B4C05460E46207810B1FC +:101780000C2004B070BD95F8652095F86410686A67 +:1017900000F0C5FEC5F80401656295F8F00000B1DF +:1017A000FFDF104900202439C861052121706070D5 +:1017B00084F82800014604E004EB4102491C5085EE +:1017C000C9B294F836208A42F6D284F83500304601 +:1017D000FFF7D5FE0548F4F74DFC84F820002028DB +:1017E00007D105E0F0110020800100207D140200E7 +:1017F000FFDFF4F7B9FC606194F82010012268461D +:10180000FFF781FC00B9FFDF94F82000694600F083 +:1018100096FD00B9FFDF0020B3E7F94810B5007866 +:1018200008B1002010BD0620F2F7DAFA80F00100BE +:1018300010BDF8B5F24D0446287800B1FFDF002056 +:10184000009023780246DE0701466B4605D060888B +:10185000A188ADF80010012211462678760706D53A +:10186000E088248923F8114042F00802491C491EEF +:1018700085F836101946FFF792FE0020F8BD1FB517 +:1018800011B1112004B010BDDD4C217809B10C203C +:10189000F8E70022627004212170114605E000BFC4 +:1018A00004EB4103491C5A85C9B294F836308B4287 +:1018B000F6D284F83520FFF762FED248F4F7DAFB5F +:1018C00084F82000202800D1FFDF00F0DDFD10B1FA +:1018D000F4F74AFC05E0F4F747FC40F6B831F4F7BA +:1018E0002AF9606194F8201001226846FFF70BFC8A +:1018F00000B9FFDF94F82000694600F020FD00B930 +:10190000FFDF0020BEE770B5BD4C616A0160FFF7E4 +:10191000A0FE050002D1606AFFF7B0F80020606207 +:10192000284670BD7FB5B64C2178052901D00C2022 +:1019300027E7B3492439C860606A00B9FFDF606AED +:1019400090F8F00000B1FFDF606A90F8FA002028FC +:1019500000D0FFDFAC48F4F78DFB616A0546202814 +:1019600081F8FA000E8800D3FFDFA548443020F844 +:101970001560606A90F8FA00202800D1FFDF00238C +:1019800001226846616AFFF794F8606A694690F838 +:10199000FA0000F0D4FC00B9FFDF00206062F0E63E +:1019A000974924394870704710B540F2E24300FB74 +:1019B00003F4002000F0B3FD844201D9201A10BDC9 +:1019C000002010BD70B50D46064601460020FCF70C +:1019D000E0FE044696F86500F6F706FB014696F829 +:1019E00064004FF47A72022815D0012815D040F611 +:1019F000340008444AF247310844B0FBF2F17088E1 +:101A000040F271225043C1EB4000A0F22630A542C3 +:101A100006D2214605E01046EBE74FF4C860E8E740 +:101A20002946814204D2A54201D2204600E0284640 +:101A3000706270BD70B50546FDF7E0FB7049007837 +:101A400024398C689834072D30D2DFE805F004344F +:101A500034252C34340014214FF4A873042810D0FA +:101A60000822082809D02A2102280FD011FB0240A1 +:101A700000222823D118441819E0402211FB02400B +:101A8000F8E7102211FB02402E22F3E7042211FB9B +:101A9000024000221823EDE7282100F04BFC04440B +:101AA00004F5317403E004F5B07400E0FFDF54483E +:101AB000C06BA04201D9012070BD002070BD70B57F +:101AC0004F4C243C607870B1D4E904512846A26898 +:101AD000EFF7EDFA2061A84205D0A169401B084448 +:101AE000A061F5F706F82169A068884201D820783E +:101AF00008B1002070BD012070BD2DE9F04F0546F2 +:101B000085B016460F461C461846F6F7F5FA05EB63 +:101B100047014718204600F0F5FB4AF2C5714FF423 +:101B20007A7908444D46B0FBF5F0384400F160087E +:101B30003348761C24388068304404902046F6F7F9 +:101B4000DBFAA8EB0007204600F0DCFB0646204647 +:101B5000F6F74AFA301AB0FBF5F03A1A18252820A1 +:101B60004FF4C8764FF4BF774FF0020B082C30D0FB +:101B7000042C2BD00021022C2ED0082311F1280197 +:101B800003EB830C0CEB831319440A444FF0000A57 +:101B9000082C29D0042C22D00021022C29D0054663 +:101BA000082001F5B07100BF00EB0010284481420D +:101BB00032D2082C2AD0042C1ED00020022C28D08F +:101BC0000821283001EB0111084434E03946102384 +:101BD000D6E731464023D3E704231831D0E73D460A +:101BE00040F2EE311020DFE735464FF435614020FA +:101BF000DAE70420B431D7E738461021E2E70000E5 +:101C0000F01100207D140200530D020030464021E7 +:101C1000D8E704211830D5E7082C4FD0042C4AD03F +:101C20000021022C4DD0082311F12801C3EBC30081 +:101C300000EB4310084415182821204600F07AFBD9 +:101C400005EB4001082C42D0042C3DD00026022C8C +:101C50003FD0082016F1280600EB801006EB80002C +:101C60000E180120FA4D8DF804008DF800A08DF8B3 +:101C700005B0A86906F22A260499F3F75CFFCDE9BE +:101C800002062046F6F7B0F94AF23B510144B1FB97 +:101C9000F9F0301AFE38E8630298C5F84080A86170 +:101CA00095F82000694600F04AFB002800D1FFDFCC +:101CB00005B0BDE8F08F39461023B7E73146402321 +:101CC000B4E704231831B1E73E461020C4E74020B2 +:101CD000C2E704201836BFE72DE9FE4F06461C4632 +:101CE000174688464FF0010A1846F6F705FAD84D10 +:101CF000243DA9688A1907EB48011144471820467A +:101D000000F000FB4FF47A7BD84600F6FB00B0FBF6 +:101D1000F8F0384400F120092046F6F7EDF9A968FB +:101D20000246A9EB0100801B871A204600F0EAFA60 +:101D300005462046F6F758F9281AB0FBF8F03A1A8B +:101D4000182528204FF4C8774FF4BF78082C2DD0E1 +:101D5000042C28D00021022C2BD0082311F12801BB +:101D600003EB830C0CEB831319440A44082C28D092 +:101D7000042C21D00021022C28D00546082001F592 +:101D8000B07100BF00EB0010284481422AD2082C19 +:101D900022D0042C1DD00020022C20D00821283075 +:101DA00001EB01112CE041461023D9E739464023CD +:101DB000D6E704231831D3E7454640F2EE31102030 +:101DC000E0E73D464FF435614020DBE70420B431C5 +:101DD000D8E740461021E3E738464021E0E70421F8 +:101DE0001830DDE7082C48D0042C43D00020022C0A +:101DF00046D0082110F12800C1EBC10303EB4111CB +:101E0000084415182821204600F094FA05EB4001FB +:101E1000082C3BD0042C36D00027022C38D00820C8 +:101E200017F1280700EB801007EB80000C1804F571 +:101E300096740C98F6F7D8F84AF23B510144B1FB7E +:101E4000FBF0834DFE30A5F12407E96B06F1FE029D +:101E50000844B9680B191A44824224D93219114432 +:101E60000C1AFE342044B0F1807F37D2642C12D299 +:101E7000642011E040461021BEE738464021BBE710 +:101E800004211830B8E747461020CBE74020C9E7C7 +:101E900004201837C6E720460421F4F790FEE8B185 +:101EA000E86B2044E863E0F703FFB9682938314460 +:101EB0000844CDE9000995F835008DF808000220A6 +:101EC0008DF809006846FCF778FE00B1FFDFFCF7EB +:101ED00063FF00B1FFDF5046BDE8FE8F4FF0000A00 +:101EE000F9E71FB500F021FB594C607880B994F8F0 +:101EF000201000226846FFF706F938B194F8200058 +:101F0000694600F01CFA18B9FFDF01E00120E0701B +:101F1000F4F735F800206074A0741FBD2DE9F84F68 +:101F2000FDF76CF90646451CC07840090CD0012825 +:101F30000CD002280CD000202978824608064FF4E5 +:101F4000967407D41E2006E00120F5E70220F3E78F +:101F50000820F1E72046B5F80120C2F30C0212FB7D +:101F600000F7C80901D010B103E01E2401E0FFDF33 +:101F70000024F6F7D3F8A7EB00092878B77909EB26 +:101F80000408C0F3801010B120B1322504E04FF4F2 +:101F9000FA7501E0FFDF00250C2F00D3FFDF2D488D +:101FA0002D4A30F81700291801FB0821501CB1FBFD +:101FB000F0F5F6F76DF8F6F717F84FF47A7100F2CE +:101FC0007160B0FBF1F1A9EB0100471BA7F15900CB +:101FD000103FB0F5247F11D31D4E717829B9024608 +:101FE000534629462046FFF788FD00F09EFAF3F796 +:101FF000C6FF00207074B074BDE8F88F3078009090 +:102000005346224629463846FFF766FE0028F3D19C +:1020100001210220FDF7F6F8BDE8F84F61E710B5A1 +:102020000446012903D10A482438007830B104203D +:1020300084F8F000BDE81040F3F7A1BF00220121B1 +:10204000204600F0A5F934F8700F401C2080F1E71D +:10205000F0110020646302003F420F002DE9F041BF +:102060000746FDF7CBF8050000D1FFDF287810F018 +:102070000C0F01D0012100E00021F74C606A3030E4 +:10208000FCF7C7FA29783846EFF71BFAA4F12406C3 +:102090000146A069B26802446FB32878082803D0CB +:1020A000042803D000230BE0082300E0022303EB05 +:1020B000430328334FF4A877082804D0042802D01B +:1020C000022810D028273B4408280ED004280ED020 +:1020D00002280ED05FF00800C0EBC00707EB4010ED +:1020E0001844983009E01827EDE74020F4E7102065 +:1020F000F2E70420F0E74FF4FC701044471828780A +:102100003F1DF5F771FF014628784FF47A720228D7 +:102110001DD001281DD040F6340008444AF2EF01DA +:102120000844B0FBF2F03A1A606A40F2E241B0466D +:102130004788F0304F43316A81420DD03946206BD9 +:1021400000F08EF90646B84207D9FFDF05E01046D9 +:10215000E3E74FF4C860E0E70026C04880688642A5 +:1021600007D2616A40F271224888424306EB420678 +:1021700004E040F2E240B6FBF0F0616AC882606AB7 +:10218000297880F86410297880F865100521417558 +:10219000C08A6FF41C71484306EB400040F635419D +:1021A000C8F81C00B0EB410F00D3FFDFBDE8F081A1 +:1021B00010B5052937D2DFE801F00509030D31001C +:1021C000002100E00121BDE8104028E7032180F84C +:1021D000F01010BD0446408840F2E24148439F4958 +:1021E000091D0860D4F818010089E082D4F81801AC +:1021F00080796075D4F8180140896080D4F818019E +:102200008089A080D4F81801C089E0802046A16AA6 +:10221000FFF7D8FB022084F8F00010BD816ABDE80A +:102220001040FFF7CFBBFFDF10BD70B58A4C243CD8 +:102230000928A1683FD2DFE800F0050B0B15131544 +:1022400038380800BDE870404BE6BDE8704065E6F0 +:10225000022803D00020BDE87040FFE60120FAE725 +:10226000E16070BD032802D005281CD000E0E160C9 +:102270005FF0000600F059F9774D012085F828003D +:1022800085F83460686AA9690026C0F8F41080F8FF +:10229000F060E068FFF746FB00B1FFDFF3F76FFE89 +:1022A0006E74AE7470BD0126E4E76C480078BDE83A +:1022B0007040E0F729BEFFDF70BD674924394860F0 +:1022C000704770B5644D0446243DB1B14FF47A7641 +:1022D000012903D0022905D0FFDF70BD1846F5F7AC +:1022E000FCFE05E06888401C68801046F6F7F8FFA1 +:1022F00000F2E730B0FBF6F0201AA86070BD564837 +:1023000000787047082803D0042801D0F5F76CBE88 +:102310004EF628307047002804DB00F1E02090F8EA +:10232000000405E000F00F0000F1E02090F8140D2B +:102330004009704710F00C0000D008467047F4F7D1 +:102340003EB910B50446202800D3FFDF4248443090 +:1023500030F8140010BD70B505460C461046F5F770 +:1023600043FE4FF47A71022C0DD0012C0DD040F6B3 +:10237000340210444AF247321044B0FBF1F02844D2 +:1023800000F5CB7070BD0A46F3E74FF4C862F0E782 +:102390001FB513460A46044601466846FEF789FB08 +:1023A00094F8FA006946FFF7CAFF002800D1FFDF62 +:1023B0001FBD70B5284C0025257094F82000F3F758 +:1023C000B4FE00B9FFDF84F8205070BD2DE9F04164 +:1023D000050000D1FFDF204A0024243AD5F804612B +:1023E0002046631E116A08E08869B04203D3984210 +:1023F00001D203460C460846C9680029F4D104B945 +:1024000004460021C5F80041F035C4B1E068E5603C +:10241000E86000B105612E698846A96156B1B069CE +:1024200030B16F69B84200D2FFDFB069C01BA8614C +:10243000C6F81880084D5CB1207820B902E0E96048 +:102440001562E8E7FFDF6169606808442863ADE66C +:10245000C5F83080AAE60000F011002080010020BD +:1024600010B50C4601461046F4F776FB002806DA54 +:10247000211A491EB1FBF4F101FB040010BD90FBD1 +:10248000F4F101FB140010BD2E48016A002001E0A8 +:102490000846C9680029FBD170472DE9FE43294D44 +:1024A0000120287000264FF6FF7420E00621F1F786 +:1024B000FDFC070000D1FFDF97F8FA00F037F4F7D2 +:1024C00006FC07F80A6BA14617F8FA89B8F1200F45 +:1024D00000D3FFDF1B4A683222F8189097F8FA0001 +:1024E000F3F723FE00B9FFDF202087F8FA006946E2 +:1024F0000620F1F764FC50B1FFDF08E0029830B12C +:1025000090F8F01019B10088A042CFD104E06846DD +:10251000F1F733FC0028F1D02E70BDE8FE8310B532 +:10252000FFF719FF00F5C87010BD064800212430E0 +:1025300090F8352000EB4200418503480078E0F731 +:10254000E3BC0000CC11002080010020012804D051 +:10255000022805D0032808D105E0012907D004E0AE +:10256000022904D001E0042901D000207047012095 +:102570007047F748806890F8A21029B1B0F89E1013 +:10258000B0F8A020914215D290F8A61029B1B0F869 +:10259000A410B0F8A02091420CD2B0F89C20B0F862 +:1025A0009A108A4206D290F88020B0F898001AB1AA +:1025B000884203D3012070470628FBD200207047D1 +:1025C0002DE9F041E24D0746A86800F1700490F84B +:1025D000140130B9E27B002301212046EEF7D2FC42 +:1025E00010B1A08D401CA08501263D21AFB92878EF +:1025F000022808D001280AD06878C8B110F0140F5A +:1026000009D01E2039E0162037E026773EE0A86882 +:1026100090F8160131E0020701D56177F5E78107EF +:1026200001D02A2029E0800600D4FFDF232024E007 +:1026300094F8320028B1E08D411CE185218E88425A +:1026400013D294F8360028B1A08E411CA186218EA9 +:1026500088420AD2A18D608D814203D3AA6892F884 +:10266000142112B9228E914201D3222005E0217C4F +:1026700029B1218D814207D308206077C5E7208DDD +:10268000062801D33E20F8E7207FB0B10020207358 +:10269000607320740221A868FFF78AFDA86890F88B +:1026A000E410012904D1D0F81C110878401E0870EC +:1026B000E878BDE8F041E0F727BCA868BDE8F04144 +:1026C0000021FFF775BDA2490C28896881F8E40054 +:1026D00014D0132812D0182810D0002211280ED0A0 +:1026E00007280BD015280AD0012807D0002805D0CC +:1026F000022803D021F89E2F012008717047A1F80D +:10270000A420704710B5924CA1680A88A1F86021F6 +:1027100081F85E0191F8640001F046FBA16881F840 +:10272000620191F8650001F03FFBA16881F8630147 +:10273000012081F85C01002081F82E01E078BDE8DD +:102740001040E0F7E1BB70B5814C00231946A0684A +:1027500090F87C207030EEF715FC00283DD0A06882 +:1027600090F820110025C9B3A1690978B1BB90F890 +:102770007D00EEF7EFFB88BBA168B1F870000A2876 +:102780002DD905220831E069EBF72AFE10B3A068C5 +:10279000D0F81C11087858B10522491CE069EBF704 +:1027A0001FFE002819D1A068D0F81C01007840B99C +:1027B000A068E169D0F81C010A68C0F80120097915 +:1027C0004171A068D0F81C110878401C08700120E5 +:1027D000FFF779FFA06880F8205170BDFFE7A0687F +:1027E00090F8241111B190F82511C1B390F82E1171 +:1027F0000029F2D090F82F110029EED190F87D0039 +:10280000EEF7A8FB0028E8D1A06890F8640001F07A +:10281000CBFA0646A06890F8650001F0C5FA0546B7 +:10282000A06890F830113046FFF790FEA0B3A06882 +:1028300090F831112846FFF789FE68B3A268B2F814 +:10284000703092F86410B2F8320102F58872EEF737 +:1028500001FE20B3A168252081F87C00BDE7FFE7D9 +:1028600090F87D10242918D090F87C10242914D0D9 +:102870005FF0000300F5897200F59271FBF78EFEA0 +:10288000A16881F8245101F13000C28A21F8E62FB5 +:10289000408B4880142007E005E00123EAE7BDE80B +:1028A000704000202EE71620BDE870400BE710B501 +:1028B000F4F7FAFD0C2813D3254C0821A068D0F8B2 +:1028C00018011E30F4F7F4FD28B1A0680421D830B7 +:1028D000F4F7EEFD00B9FFDFBDE810400320F2E69B +:1028E00010BD10B51A4CA068D0F818110A78002A4B +:1028F0001FD04988028891421BD190F87C20002388 +:1029000019467030EEF73EFB002812D0A068D0F8D0 +:1029100018110978022907D003290BD0042919D0EE +:10292000052906D108200DE090F87D00EEF712FB96 +:1029300040B110BD90F8811039B190F8820000B913 +:10294000FFDF0A20BDE81040BDE6BDE81040AEE75D +:102950008C01002090F8AA008007EAD10C20FFF734 +:10296000B2FEA068002120F89E1F01210171017BA9 +:1029700041F00101017310BD70B5F74CA268556EAE +:10298000EEF702FDEBB2C1B200228B4203D0A36886 +:1029900083F8121102E0A16881F81221C5F3072122 +:1029A000C0F30720814203D0A16881F8130114E726 +:1029B000A06880F8132110E710B5E74C0421A06847 +:1029C000FFF7F6FBA06890F85A10012908D000F52F +:1029D0009E71FBF7ACFEE078BDE81040E0F794BADA +:1029E000022180F85A1010BD70B5DB4CA06890F839 +:1029F000E410FE2955D16178002952D190F87F204A +:102A0000002301217030EEF7BDFA002849D1A068FB +:102A100090F8141109B1022037E090F87C200023CF +:102A200019467030EEF7AEFA28B1A06890F896001B +:102A300008B1122029E0A068002590F87C20122A15 +:102A40001DD004DC032A23D0112A04D119E0182A4E +:102A50001AD0232A26D0002304217030EEF792FAF0 +:102A600000281ED1A06890F87D10192971D020DCB3 +:102A700001292AD0022935D0032932D120E00B20A8 +:102A800003E0BDE8704012E70620BDE870401AE69A +:102A900010F8E21F01710720FFF715FEA06880F80B +:102AA0007C509AE61820FFF70EFEA068A0F89E5012 +:102AB00093E61D2918D01E2916D0212966D149E098 +:102AC00010F8E11F4171072070E00C20FFF7FBFDBB +:102AD000A06820F8A45F817941F00101817100F8BC +:102AE000275C53E013202CE090F8252182BB90F85E +:102AF0002421B2B1242912D090F87C1024290ED0C0 +:102B00005FF0000300F5897200F59271FBF746FD56 +:102B1000A0681E2180F87D1080F8245103E0012375 +:102B2000F0E71E2932D1A068FBF797FDFFF744FFBD +:102B3000A16801F13000C28A21F8E62F408B48805D +:102B40001520FFF7C0FDA068A0F8A45080F87D50C4 +:102B50001CE02AE090F8971051B180F8125180F8EB +:102B600013511820FFF7AFFDA068A0F8A4500DE0A6 +:102B700090F82F1151B990F82E1139B1C16DD0F8DC +:102B80003001FFF7F9FE1820FFF79DFDA06890F8CF +:102B9000E400FE2885D1FFF7A4FEA06890F8E400C9 +:102BA000FE2885D1BDE87040CDE51120FFF78BFDF3 +:102BB000A068CBE7684A0129926819D0002302294E +:102BC0000FD003291ED010B301282BD0032807D122 +:102BD00092F87C00132803D0162801D0182804D1BD +:102BE000704792F8E4000028FAD0D2F8180117E0F4 +:102BF00092F8E4000128F3D0D2F81C110878401EA6 +:102C00000870704792F8E4000328EED17047D2F8BC +:102C10001801B2F870108288891A09B20029F5DB10 +:102C200003707047B2F87000B2F82211401A00B277 +:102C30000028F6DBD2F81C010178491E01707047AC +:102C400070B5044690F87C0000250C2810D00D28A3 +:102C50002ED1D4F81811B4F870008988401C88422D +:102C600026D1D4F864013C4E017811B3FFDF42E075 +:102C7000B4F87000B4F82211401C884218D1D4F87E +:102C80001C01D0F80110A160407920730321204677 +:102C9000EDF7F1FDD4F81C01007800B9FFDF012148 +:102CA000FE20FFF787FF84F87C50012084F8B200F3 +:102CB00093E52188C180D4F81801D4F864114089C3 +:102CC0000881D4F81801D4F8641180894881D4F8B7 +:102CD0001801D4F86411C0898881D4F864010571A1 +:102CE000D4F8641109200870D4F864112088488051 +:102CF000F078E0F709F902212046EDF7BCFD032149 +:102D00002046FFF755FAB068D0F81801007802287D +:102D100000D0FFDF0221FE20FFF74CFF84F87C503B +:102D20005BE52DE9F0410C4C00260327D4F808C0E0 +:102D3000012598B12069C0788CF8E20005FA00F00E +:102D4000C0F3C05000B9FFDFA06800F87C7F468464 +:102D500080F82650BDE8F0818C01002000239CF80B +:102D60007D2019460CF17000EEF70CF970B1607817 +:102D70000028EFD12069C178A06880F8E11080F8C0 +:102D80007D70A0F8A46080F8A650E3E76570E1E7E5 +:102D9000F0B5F74C002385B0A068194690F87D2067 +:102DA0007030EEF7EFF8012580B1A06890F87C0054 +:102DB00023280ED024280CD06846F6F785FB68B18E +:102DC000009801A9C0788DF8040008E0657005B08E +:102DD000F0BD607840F020006070F8E70021A06846 +:102DE00003AB162290F87C00EEF74FFB002648B1AB +:102DF000A0689DF80C20162180F80C2180F80D1198 +:102E0000192136E02069FBF722FB78B121690879A6 +:102E100000F00702A06880F85C20497901F0070102 +:102E200080F85D1090F82F310BBB03E00020FFF716 +:102E300078FFCCE790F82E31CBB900F164035F78CE +:102E4000974205D11A788A4202D180F897500EE055 +:102E500000F5AC71028821F8022990F85C200A7113 +:102E600090F85D0048710D70E078E0F74DF8A068CB +:102E7000212180F87D1080F8A650A0F8A460A6E774 +:102E8000F8B5BB4C00231946A06890F87D2070303F +:102E9000EEF778F840B32069FBF7BEFA48B3206933 +:102EA000FBF7B4FA07462069FBF7B4FA0646206937 +:102EB000FBF7AAFA05462069FBF7AAFA0146009734 +:102EC000A06833462A463030FBF79BFBA1680125FA +:102ED00091F87C001C2810D091F85A00012812D0DB +:102EE00091F8250178B90BE0607840F0010060703E +:102EF000F8BDBDE8F840002013E781F85A5002E021 +:102F000091F8240118B11E2081F87D000BE01D20EE +:102F100081F87D0001F5A57231F8300BFBF7FBFB62 +:102F2000E078DFF7F1FFA068002120F8A41F85708A +:102F3000F8BD10B58E4C00230921A06890F87C20C4 +:102F40007030EEF71FF848B16078002805D1A1680D +:102F500001F8960F087301F81A0C10BD012060707B +:102F600010BD7CB5824C00230721A06890F87C201E +:102F70007030EEF707F838B36078002826D169463C +:102F80002069FBF75FFA9DF80000002500F025019D +:102F9000A06880F8B0109DF8011001F0490180F898 +:102FA000B11080F8A250D0F81811008849888142E9 +:102FB00000D0FFDFA068D0F818110D70D0F86411B0 +:102FC0000A7822B1FFDF16E0012060707CBD30F886 +:102FD000E82BCA80C16F0D71C16F009A8A60019A97 +:102FE000CA60C26F0821117030F8E81CC06F4180C0 +:102FF000E078DFF789FFA06880F87C507CBD70B571 +:103000005B4C00231946A06890F87D207030EDF7E6 +:10301000B9FF012540B9A0680023082190F87C2061 +:103020007030EDF7AFFF10B36078002820D1A068B2 +:1030300090F8AA00800712D42069FBF7C9F9A168AB +:1030400081F8AB00206930F8052FA1F8AC2040884A +:10305000A1F8AE0011F8AA0F40F002000870A068B5 +:103060004FF0000690F8AA10C90702D011E0657071 +:103070009DE490F87D20002319467030EDF782FF23 +:1030800000B9FFDFA06880F87D5080F8A650A0F856 +:10309000A460A06890F87C10012906D180F87C60BB +:1030A00080F8A260E078DFF72FFFA168D1F818015F +:1030B000098842888A42DBD101780429D8D1067078 +:1030C000E078DFF721FFA06890F87C100029CFD1CD +:1030D00080F8A2606BE470B5254DA86890F87C106C +:1030E0001A2902D00220687061E469780029FBD1B6 +:1030F000002480F8A74080F8A240D0F8181100887A +:103100004988814200D0FFDFA868D0F818110C7000 +:10311000D0F864110A780AB1FFDF25E090F8A82002 +:1031200072B180F8A8400288CA80D0F864110C718E +:10313000D0F864210E2111700188D0F864010DE0EF +:1031400030F8E82BCA80C16F0C71C26F0121117277 +:10315000C26F0D21117030F8E81CC06F418000F083 +:10316000A1FEE878DFF7D0FEA86880F87C401EE476 +:103170008C01002070B5FA4CA16891F87C20162AC9 +:1031800001D0132A02D191F8A82012B10220607058 +:103190000DE46278002AFBD181F8E000002581F877 +:1031A000A75081F8A250D1F81801098840888842B8 +:1031B00000D0FFDFA068D0F818010078032800D005 +:1031C000FFDF0321FE20FFF7F5FCA068D0F86411B3 +:1031D0000A780AB1FFDF14E030F8E02BCA8010F85B +:1031E000081BC26F1171C16F0D72C26F0D2111707A +:1031F00030F8E81CC06F418000F054FEE078DFF743 +:1032000083FEA06880F87C504BE470B5D44C092153 +:103210000023A06890F87C207030EDF7B3FE002505 +:1032200018B12069007912281ED0A0680A21002355 +:1032300090F87C207030EDF7A5FE18B12069007978 +:10324000142814D02069007916281AD1A06890F8A3 +:103250007C101F2915D180F87C5080F8A250BDE861 +:1032600070401A20FFF74EBABDE8704061E6A068D2 +:1032700000F87C5F458480F82650BDE87040FFF779 +:103280009BBB0EE470B5B64C2079C00773D02069A3 +:1032900000230521C578A06890F87C207030EDF7F8 +:1032A00071FE98B1062D11D006DC022D0ED0042D32 +:1032B0000CD0052D06D109E00B2D07D00D2D05D022 +:1032C000112D03D0607840F008006070607800280D +:1032D00051D12069FAF7E0FF00287ED0206900254F +:1032E0000226C178891E162977D2DFE801F00B7615 +:1032F00034374722764D76254A457676763A5350CE +:103300006A6D7073A0680023012190F87F207030EF +:10331000EDF738FE08BB2069FBF722F8A16881F8B9 +:103320001601072081F87F0081F8A65081F8A2508D +:1033300056E0FFF76AFF53E0A06890F87C100F2971 +:1033400001D066704CE0617839B980F88150122163 +:1033500080F87C1044E000F0D0FD41E000F0ACFDCE +:103360003EE0FBF7A9F803283AD12069FBF7A8F85B +:10337000FFF700FF34E03BE00079F9E7FFF7ABFE31 +:103380002EE0FFF73CFE2BE0FFF7EBFD28E0FFF718 +:10339000D0FD25E0A0680023194690F87D2070300C +:1033A000EDF7F0FD012110B16078C8B901E061705E +:1033B00016E0A06820F8A45F817000F8276C0FE089 +:1033C0000BE0FFF75DFD0BE000F034FD08E0FFF7D8 +:1033D000DFFC05E000F0FAFC02E00020FFF7A1FCB2 +:1033E000A268F2E93001401C41F10001C2E900018C +:1033F0005EE42DE9F0415A4C2079800741D5607890 +:1034000000283ED1E06801270026C178204619290E +:10341000856805F170006FD2DFE801F04B3E0D6F5B +:10342000C1C1801C34C1556287C1C1C1C1BE8B9569 +:1034300098A4B0C1BA0095F87F2000230121EDF7D0 +:10344000A1FD00281DD1A068082180F87F1080F818 +:10345000A26090E0002395F87D201946EDF792FDDB +:1034600010B1A06880F8A660A0680023194690F803 +:103470007C207030EDF786FD002802D0A06880F82F +:10348000A26067E4002395F87C201946EDF77AFDE9 +:1034900000B9FFDF042008E0002395F87C201946DE +:1034A000EDF770FD00B9FFDF0C20A16881F87C000A +:1034B00050E4002395F87C201946EDF763FD00B930 +:1034C000FFDF0D20F1E7002395F87C201946EDF78A +:1034D00059FD00B9FFDFA0680F2180F8A77008E050 +:1034E00095F87C00122800D0FFDFA068112180F839 +:1034F000A87080F87C102DE451E0002395F87C2022 +:103500001946EDF73FFD20B9A06890F8A80000B972 +:10351000FFDFA068132180F8A770EAE795F87C0028 +:10352000182800D0FFDF1A20BFE7BDE8F04100F007 +:1035300063BD002395F87C201946EDF723FD00B903 +:10354000FFDF0520B1E785F8A66003E4002395F8C6 +:103550007C201946EDF716FD00B9FFDF1C20A4E71B +:103560008C010020002395F87D201946EDF70AFD17 +:1035700000B9FFDFA06880F8A66006E4002395F894 +:103580007C201946EDF7FEFC00B9FFDF1F208CE719 +:10359000BDE8F04100F0F8BC85F87D60D3E7FFDFBF +:1035A0006FE710B5F74C6078002837D120794007D5 +:1035B0000FD5A06890F87C00032800D1FFDFA06839 +:1035C00090F87F10072904D101212170002180F893 +:1035D0007F10FFF70EFF00F0B5FCFFF753FEA07859 +:1035E000000716D5A0680023052190F87C207030D4 +:1035F000EDF7C8FC50B108206070A068D0F86411E5 +:1036000008780D2800D10020087002E00020F9F7AA +:10361000F9FCA068BDE81040FFF712BB10BD2DE912 +:10362000F041D84C07464FF00005607808436070C1 +:10363000207981062046806802D5A0F8985004E0E1 +:10364000B0F89810491CA0F8981000F018FD012659 +:10365000F8B1A088000506D5A06890F8821011B1D5 +:10366000A0F88E5015E0A068B0F88E10491CA0F8A4 +:103670008E1000F0F3FCA068B0F88E10B0F8902027 +:10368000914206D3A0F88E5080F83A61E078DFF7D7 +:103690003BFC207910F0600F08D0A06890F88010F3 +:1036A00021B980F880600121FEF782FD1FB9FFF784 +:1036B00078FFFFF799F93846FEF782FFBDE8F04141 +:1036C000F5F711BFAF4A51789378194313D11146DA +:1036D0000128896808D01079400703D591F87F0048 +:1036E000072808D001207047B1F84C00098E8842A5 +:1036F00001D8FEF7E4B900207047A249C278896872 +:10370000012A06D05AB1182A08D1B1F81011FAF7D7 +:10371000BABEB1F822114172090A81727047D1F81C +:10372000181189884173090A8173704770B5954CE7 +:1037300005460E46A0882843A080A80703D5E807C1 +:1037400000D0FFDFE660E80700D02661A80719D5A2 +:10375000F078062802D00B2814D10BE0A06890F86E +:103760007C1018290ED10021E0E93011012100F868 +:103770003E1C07E0A06890F87C10122902D10021BD +:1037800080F88210280601D50820A07068050AD5A7 +:10379000A0688288B0F87010304600F07FFC304698 +:1037A000BDE87040A9E763E43EB505466846F5F715 +:1037B00065FE00B9FFDF222200210098EAF767FECC +:1037C00003210098FAF750FD0098017821F01001CC +:1037D00001702946FAF76DFD6A4C192D71D2DFE8A8 +:1037E00005F020180D3EC8C8C91266C8C9C959C815 +:1037F000C8C8C8BBC9C971718AC89300A1680098BC +:1038000091F8151103E0A168009891F8E610017194 +:10381000B0E0A068D0F81C110098491CFAF795FD9B +:10382000A8E0A1680098D1F8182192790271D1F826 +:10383000182112894271120A8271D1F81821528915 +:10384000C271120A0272D1F8182192894272120AC8 +:103850008272D1F81811C989FAF74EFD8AE0A06882 +:10386000D0F818110098091DFAF77CFDA068D0F86F +:10387000181100980C31FAF77FFDA068D0F81811E4 +:1038800000981E31FAF77EFDA1680098D831FAF74A +:1038900087FD6FE06269009811780171918841712C +:1038A000090A81715188C171090A017262E03649C1 +:1038B000D1E90001CDE9010101A90098FAF78AFDDB +:1038C00058E056E0A068B0F844100098FAF794FD6C +:1038D000A068B0F8E6100098FAF792FDA068B0F87A +:1038E00048100098FAF780FDA068B0F8E81000983A +:1038F000FAF77EFD3EE0A168009891F83021027150 +:1039000091F83111417135E0A06890F81301EDF79D +:1039100032FD01460098FAF7B2FDA06890F8120156 +:1039200000F03DFA70B1A06890F8640000F037FA3A +:1039300040B1A06890F8121190F86400814201D063 +:10394000002002E0A06890F81201EDF714FD014696 +:103950000098FAF790FD0DE0A06890F80D1100981E +:10396000FAF7A8FDA06890F80C110098FAF7A6FDE8 +:1039700000E0FFDFF5F795FD00B9FFDF0098FFF7E6 +:10398000BCFE3EBD8C0100207C63020010B5F94CEA +:10399000A06890F8121109B990F8641080F86410CA +:1039A00090F8131109B990F8651080F8651000209F +:1039B000FEF7A8FEA068FAF750FE002806D0A0681F +:1039C000BDE8104000F59E71FAF7B1BE10BDF8B524 +:1039D000E84E00250446B060B5807570B57035704E +:1039E0000088F5F748FDB0680088F5F76AFDB4F87F +:1039F000F800B168401C82B201F17000EDF75BF88D +:103A000000B1FFDF94F87D00242809D1B4F87010CC +:103A1000B4F81001081A00B2002801DB707830B148 +:103A200094F87C0024280AD0252808D015E0FFF758 +:103A3000ADFF84F87D50B16881F897500DE0B4F87F +:103A40007010B4F81001081A00B2002805DB707875 +:103A500018B9FFF79BFF84F87C50A4F8F850FEF7E4 +:103A600088FD00281CD1B06890F8E400FE2801D041 +:103A7000FFF79AFEC0480090C04BC14A2146284635 +:103A8000F9F7ECF9B0680023052190F87C2070303C +:103A9000EDF778FA002803D0BDE8F840F8F771BFD9 +:103AA000F8BD10B5FEF765FD20B10020BDE810405F +:103AB0000146B4E5BDE81040F9F77FBA70B50C4691 +:103AC000154606464FF4B47200212046EAF7DFFCA3 +:103AD000268005B9FFDF2868C4F818016868C4F8B3 +:103AE0001C01A868C4F8640182E4F0F7E9BA2DE982 +:103AF000F0410D4607460621F0F7D8F9041E3DD0E7 +:103B0000D4F864110026087858B14A8821888A427E +:103B100007D109280FD00E2819D00D2826D0082843 +:103B20003ED094F83A01D0B36E701020287084F81B +:103B30003A61AF809AE06E7009202870D4F8640171 +:103B4000416869608168A9608089A88133E008467E +:103B5000F0F7DDFA0746EFF78AFF70B96E700E20B6 +:103B60002870D4F864014068686011E00846F0F7F6 +:103B7000CEFA0746EFF77BFF08B1002081E46E70B4 +:103B80000D202870D4F8640141686960008928819B +:103B9000D4F8640106703846EFF763FF66E00EE084 +:103BA0006E7008202870D4F86401416869608168EB +:103BB000A960C068E860D4F86401067056E094F823 +:103BC0003C0198B16E70152028700AE084F83C61C1 +:103BD000D4F83E016860D4F84201A860D4F84601E8 +:103BE000E86094F83C010028F0D13FE094F84A01E5 +:103BF00058B16E701C20287084F84A610A2204F5BE +:103C0000A671281DEAF719FC30E094F8560140B17E +:103C10006E701D20287084F85661D4F858016860D1 +:103C200024E094F8340168B16E701A20287004E022 +:103C300084F83461D4F83601686094F834010028BF +:103C4000F6D113E094F85C01002897D06E7016202E +:103C5000287007E084F85C61D4F85E016860B4F80D +:103C60006201288194F85C010028F3D1012008E466 +:103C7000404A5061D170704770B50D4604464EE021 +:103C8000B4F8F800401CA4F8F800B4F89800401C00 +:103C9000A4F89800204600F0F2F9B8B1B4F88E000C +:103CA000401CA4F88E00204600F0D8F9B4F88E002D +:103CB000B4F89010884209D30020A4F88E000120A7 +:103CC00084F83A012B48C078DFF71EF994F8A20077 +:103CD00020B1B4F89E00401CA4F89E0094F8A60001 +:103CE00020B1B4F8A400401CA4F8A40094F8140176 +:103CF00040B994F87F200023012104F17000EDF712 +:103D000041F920B1B4F89C00401CA4F89C00204666 +:103D1000FEF796FFB4F87000401CA4F870006D1E0A +:103D2000ADB2ADD23FE5134AC2E90601704770B5A6 +:103D30000446B0F8980094F88010D1B1B4F89A1005 +:103D40000D1A2D1F94F8960040B194F87C200023A2 +:103D5000092104F17000EDF715F9B8B1B4F88E60DF +:103D6000204600F08CF980B1B4F89000801B001F51 +:103D70000CE007E08C0100201F360200C53602006F +:103D80002D370200C0F10205DCE72846A84200DA20 +:103D90000546002D01DC002005E5A8B203E510F082 +:103DA0000C0000D00120704710B5012808D002286F +:103DB00008D0042808D0082806D0FFDF204610BD10 +:103DC0000124FBE70224F9E70324F7E770B5CC4CA4 +:103DD000A06890F87C001F2804D0607840F00100B3 +:103DE0006070E0E42069FAF73CFBD8B12069012259 +:103DF0000179407901F0070161F30705294600F0D8 +:103E0000070060F30F21A06880F8A2200022A0F82C +:103E10009E20232200F87C2FD0F8B400BDE870402B +:103E2000FEF7AABD0120FEF77CFFBDE870401E2012 +:103E3000FEF768BCF8B5B24C00230A21A06890F8E0 +:103E40007C207030EDF79EF838B32069FAF7E4FA79 +:103E5000C8B12069FAF7DAFA07462069FAF7DAFA00 +:103E600006462069FAF7D0FA05462069FAF7D0FA33 +:103E700001460097A06833462A463030FAF7C1FB66 +:103E8000A068FAF7EAFBA168002081F8A20081F897 +:103E90007C00BDE8F840FEF78FBD607840F001007F +:103EA0006070F8BD964810B580680088F0F72FF96B +:103EB000BDE81040EFF7C6BD10B5914CA36893F86C +:103EC0007C00162802D00220607010BD60780028A7 +:103ED000FBD1D3F81801002200F11E010E30C833C7 +:103EE000ECF7C2FFA0680021C0E92E11012180F883 +:103EF0008110182180F87C1010BD10B5804CA0688E +:103F000090F87C10132902D00220607010BD6178F7 +:103F10000029FBD1D0F8181100884988814200D0CF +:103F2000FFDFA068D0F8181120692631FAF745FAAA +:103F3000A1682069DC31FAF748FAA168162081F8F7 +:103F40007C0010BD10B56E4C207900071BD5607841 +:103F5000002818D1A068002190F8E400FEF72AFE9E +:103F6000A06890F8E400FE2800D1FFDFA068FE21E1 +:103F700080F8E41090F87F10082904D10221217004 +:103F8000002180F87F1010BD70B55D4D2421002404 +:103F9000A86890F87D20212A05D090F87C20232A5B +:103FA00018D0FFDFA0E590F8122112B990F8132184 +:103FB0002AB180F87D10A86880F8A64094E500F842 +:103FC0007D4F847690F8B1000028F4D00020FEF7F1 +:103FD00099FBF0E790F8122112B990F813212AB159 +:103FE00080F87C10A86880F8A2407DE580F87C40CD +:103FF0000020FEF787FBF5E770B5414C0025A0686F +:10400000D0F8181103884A889A4219D109780429EE +:1040100016D190F87C20002319467030ECF7B2FFDF +:1040200000B9FFDFA06890F8AA10890703D4012126 +:1040300080F87C1004E080F8A250D0F818010570D8 +:10404000A0680023194690F87D207030ECF79AFFA5 +:10405000002802D0A06880F8A65045E5B0F890206E +:10406000B0F88E108A4201D3511A00E000218288F4 +:10407000521D8A4202D3012180F89610704710B574 +:1040800090F8821041B990F87C200023062170300E +:10409000ECF778FF002800D0012010BD70B5114466 +:1040A000174D891D8CB2C078A968012806D040B18F +:1040B000182805D191F8120138B109E0A1F8224180 +:1040C00012E5D1F8180184800EE591F8131191B131 +:1040D000FFF765FE80B1A86890F86400FFF75FFE07 +:1040E00050B1A86890F8121190F86420914203D062 +:1040F00090F8130100B90024A868A0F81041F3E477 +:104100008C01002070B58F4C0829207A6CD2DFE832 +:1041100001F004176464276B6B6458B1F4F7D3F8AB +:10412000F5F7FFF80020A072F4F7B4F9BDE870408D +:10413000F4F758BCF5F717F9BDE87040F1F71FBF69 +:10414000DEF7B6FDF5F752F8D4E90001F1F7F3FC1C +:104150002060A07A401CC0B2A072282824D370BD71 +:10416000A07A0025401EC6B2E0683044F4F700FD96 +:1041700010B9E1687F208855A07A272828BF01253B +:10418000DEF796FDA17A01EB4102C2EB81110844F2 +:104190002946F5F755F8A07A282809D2401CC0B264 +:1041A000A072282828BF70BDBDE87040F4F772B92E +:1041B000207A002818BF00F086F8F4F74BFBF4F7DC +:1041C000F7FBF5F7D0F80120E0725F480078DEF7E2 +:1041D0009BFEBDE87040F1F7D2BE002808BF70BD5D +:1041E000BDE8704000F06FB8FFDF70BD10B5554CF2 +:1041F000207A002804BF0C2010BD00202072E0723D +:10420000607AF2F7F8FA607AF2F761FD607AF1F716 +:104210008CFF00280CBF1F20002010BD002270B5AD +:10422000484C06460D46207A68B12272E272607AE6 +:10423000F2F7E1FA607AF2F74AFD607AF1F775FF7A +:10424000002808BFFFDF4048E560067070BD70B50C +:10425000050007D0A5F5E8503C494C3881429CBF89 +:10426000122070BD374CE068002804BF092070BDE3 +:10427000207A00281CBF0C2070BD3548F1F7FAFEEB +:104280006072202804BF1F2070BDF1F76DFF206011 +:104290001DB12946F1F74FFC2060012065602072B6 +:1042A00000F011F8002070BD2649CA7A002A04BF28 +:1042B000002070471E22027000224270CB684360CB +:1042C000CA7201207047F0B585B0F1F74DFF1D4D62 +:1042D0000746394668682C6800EB80004600204697 +:1042E000F2F73AFCB04206DB6868811B3846F1F70A +:1042F00022FC0446286040F2367621463846F2F722 +:104300002BFCB04204DA31463846F1F714FC04467F +:1043100000208DF8000040F6E210039004208DF894 +:10432000050001208DF8040068460294F2F7CAF8EF +:10433000687A6946F2F743F9002808BFFFDF05B045 +:10434000F0BD000074120020AC010020B5EB3C0071 +:10435000054102002DE9F0410C4612490D68114A51 +:10436000114908321160A0F12001312901D3012047 +:104370000CE0412810D040CC0C4F94E80E0007EB25 +:104380008000241F50F8807C3046B84720600548E4 +:10439000001D0560BDE8F081204601F0EBFCF5E76B +:1043A00006207047100502400100000184630200EE +:1043B00010B55548F2F7FAFF00B1FFDF5248401C34 +:1043C000F2F7F4FF002800D0FFDF10BD2DE9F14F18 +:1043D0004E4E82B0D6F800B001274B48F2F7EEFF00 +:1043E000DFF8248120B9002708F10100F2F7FCFF73 +:1043F000474C00254FF0030901206060C4F80051CC +:10440000C4F80451029931602060DFF808A11BE074 +:10441000DAF80000C00617D50E2000F068F8EFF3B8 +:10442000108010F0010072B600D001200090C4F896 +:104430000493D4F8000120B9D4F8040108B901F0BC +:10444000A3FC009800B962B6D4F8000118B9D4F8FA +:1044500004010028DCD0D4F804010028CCD137B105 +:10446000C6F800B008F10100F2F7A8FF11E008F16A +:104470000100F2F7A3FF0028B6D1C4F80893C4F8EE +:104480000451C4F800510E2000F031F81E48F2F734 +:10449000ABFF0020BDE8FE8F2DE9F0438DB00D4647 +:1044A000064600240DF110090DF1200818E000BFA8 +:1044B00004EB4407102255F827106846E9F7BDFFC2 +:1044C00005EB8707102248467968E9F7B6FF68468A +:1044D000FFF77CFF10224146B868E9F7AEFF641C85 +:1044E000B442E5DB0DB00020BDE8F0836EE70028A4 +:1044F00009DB00F01F02012191404009800000F11A +:10450000E020C0F880127047AD01002004E50040B3 +:1045100000E0004010ED00E0B54900200870704751 +:1045200070B5B44D01232B60B34B1C68002CFCD03C +:10453000002407E00E6806601E68002EFCD0001DF7 +:10454000091D641C9442F5D30020286018680028D7 +:10455000FCD070BD70B5A64E0446A84D3078022838 +:1045600000D0FFDFAC4200D3FFDF7169A44801290E +:1045700003D847F23052944201DD03224271491CB4 +:104580007161291BC1609E49707800F02EF90028E6 +:1045900000D1FFDF70BD70B5954C0D466178884243 +:1045A00000D0FFDF954E082D4BD2DFE805F04A041E +:1045B0001E2D4A4A4A382078022800D0FFDF032007 +:1045C0002070A078012801D020B108E0A06801F097 +:1045D00085F904E004F1080007C8FFF7A1FF0520F2 +:1045E0002070BDE87040F1F7CABCF1F7BDFD01468F +:1045F0006068F2F7B1FAB04202D2616902290BD3C6 +:104600000320F2F7FAFD12E0F1F7AEFD0146606813 +:10461000F2F7A2FAB042F3D2BDE870409AE72078F0 +:1046200002280AD0052806D0FFDF04202070BDE84C +:10463000704000F0D0B8022000E00320F2F7DDFD6A +:10464000F3E7FFDF70BD70B50546F1F78DFD684CEF +:1046500060602078012800D0FFDF694901200870E0 +:104660000020087104208D6048716448C8600220F1 +:104670002070607800F0B9F8002800D1FFDF70BD2D +:1046800010B55B4C207838B90220F2F7CCFD18B990 +:104690000320F2F7C8FD08B1112010BD5948F1F709 +:1046A000E9FC6070202804D00120207000206061A7 +:1046B00010BD032010BD2DE9F0471446054600EB60 +:1046C00084000E46A0F1040801F01BF907464FF0E4 +:1046D000805001694F4306EB8401091FB14201D2AA +:1046E000012100E0002189461CB10069B4EB900F64 +:1046F00002D90920BDE8F0872846DCF7A3FD90B970 +:10470000A84510D3BD4205D2B84503D245EA0600FC +:10471000800701D01020EDE73046DCF793FD10B99B +:10472000B9F1000F01D00F20E4E73748374900689E +:10473000884205D0224631462846FFF7F1FE1AE0AE +:10474000FFF79EFF0028D5D1294800218560C0E9E8 +:1047500003648170F2F7D3FD08B12D4801E04AF2FD +:10476000F87060434FF47A7100F2E730B0FBF1F07B +:104770001830FFF768FF0020BCE770B505464FF022 +:10478000805004696C432046DCF75CFD08B10F20C3 +:1047900070BD01F0B6F8A84201D8102070BD1A48CB +:1047A0001A490068884203D0204601F097F810E0CB +:1047B000FFF766FF0028F1D10D4801218460817068 +:1047C000F2F79DFD08B1134800E013481830FFF7D9 +:1047D0003AFF002070BD10B5054C6078F1F7A5FCDC +:1047E00000B9FFDF0020207010BDF1F7E8BE000027 +:1047F000B001002004E5014000E40140105C0C0021 +:1048000084120020974502005C000020BEBAFECA58 +:1048100050280500645E0100A85B01007E4909681C +:104820000160002070477C4908600020704701212A +:104830008A0720B1012804D042F204007047916732 +:1048400000E0D1670020704774490120086042F2FF +:104850000600704708B50423704A1907103230B1BA +:10486000C1F80433106840F0010010600BE01068DC +:1048700020F001001060C1F808330020C1F80801E1 +:10488000674800680090002008BD011F0B2909D867 +:10489000624910310A6822F01E0242EA40000860B4 +:1048A0000020704742F2050070470F2809D85B4985 +:1048B00010310A6822F4706242EA00200860002089 +:1048C000704742F205007047000100F18040C0F8D7 +:1048D000041900207047000100F18040C0F8081959 +:1048E00000207047000100F18040D0F80009086006 +:1048F00000207047012801D907207047494A52F823 +:10490000200002680A43026000207047012801D994 +:1049100007207047434A52F8200002688A43026029 +:1049200000207047012801D9072070473D4A52F8FE +:104930002000006808600020704702003A494FF0EC +:10494000000003D0012A01D0072070470A60704799 +:10495000020036494FF0000003D0012A01D00720A1 +:1049600070470A60704708B54FF40072510510B1E6 +:10497000C1F8042308E0C1F808230020C1F824018D +:1049800027481C3000680090002008BD08B5802230 +:10499000D10510B1C1F8042308E0C1F808230020B4 +:1049A000C1F81C011E48143000680090002008BDAA +:1049B00008B54FF48072910510B1C1F8042308E0E6 +:1049C000C1F808230020C1F82001154818300068FC +:1049D0000090002008BD10493831096801600020AE +:1049E000704770B54FF080450024C5F80841F2F7D4 +:1049F00092FC10B9F2F799FC28B1C5F82441C5F82A +:104A00001C41C5F820414FF0E020802180F80014BF +:104A10000121C0F8001170BD0004004000050040F5 +:104A2000080100404864020078050040800500400D +:104A30006249634B0A6863499A42096801D1C1F32C +:104A400010010160002070475C495D4B0A685D49B8 +:104A5000091D9A4201D1C0F3100008600020704780 +:104A60005649574B0A68574908319A4201D1C0F359 +:104A7000100008600020704730B5504B504D1C6846 +:104A800042F20803AC4202D0142802D203E01128FB +:104A900001D3184630BDC3004B481844C0F8101568 +:104AA000C0F81425002030BD4449454B0A6842F245 +:104AB00009019A4202D0062802D203E0042801D359 +:104AC00008467047404A012142F8301000207047E4 +:104AD0003A493B4B0A6842F209019A4202D0062841 +:104AE00002D203E0042801D308467047364A012168 +:104AF00002EBC00041600020704770B52F4A304E75 +:104B0000314C156842F2090304EB8002B54204D02F +:104B1000062804D2C2F8001807E0042801D318467A +:104B200070BDC1F31000C2F80008002070BD70B560 +:104B3000224A234E244C156842F2090304EB8002FA +:104B4000B54204D0062804D2D2F8000807E00428B1 +:104B500001D3184670BDD2F80008C0F310000860F9 +:104B6000002070BD174910B50831184808601120A1 +:104B7000154A002102EBC003C3F81015C3F8141541 +:104B8000401C1428F6D3002006E0042804D302EBCE +:104B90008003C3F8001807E002EB8003D3F8004855 +:104BA000C4F31004C3F80048401C0628EDD310BD20 +:104BB0000449064808310860704700005C00002086 +:104BC000BEBAFECA00F5014000F001400000FEFF41 +:104BD000814B1B6803B19847BFF34F8F7F48016833 +:104BE0007F4A01F4E06111430160BFF34F8F00BFC2 +:104BF000FDE710B5EFF3108010F0010F72B601D091 +:104C0000012400E0002400F0DDF850B1DCF7BFFB28 +:104C1000F1F755F8F2F793FAF2F7BEFF7149002069 +:104C2000086004B962B6002010BD2DE9F0410C46C1 +:104C30000546EFF3108010F0010F72B601D0012687 +:104C400000E0002600F0BEF820B106B962B60820E8 +:104C5000BDE8F08101F01EF9DCF79DFB0246002063 +:104C600001234709BF0007F1E02700F01F01D7F833 +:104C70000071CF40F9071BD0202803D222FA00F19F +:104C8000C90727D141B2002904DB01F1E02191F8E5 +:104C9000001405E001F00F0101F1E02191F8141D6D +:104CA0004909082916D203FA01F717F0EC0F11D0C1 +:104CB000401C6428D5D3F2F74DFF4B4A4B490020E6 +:104CC000F2F790FF47494A4808602046DCF7C1FAEE +:104CD00060B904E006B962B641F20100B8E73E48A7 +:104CE00004602DB12846DCF701FB18B1102428E040 +:104CF000404D19E02878022802D94FF4805420E072 +:104D000007240028687801D0D8B908E0C8B1202865 +:104D100017D8A878212814D8012812D001E0A87843 +:104D200078B9E8780B280CD8DCF735FB2946F2F780 +:104D3000ECF9F0F783FF00F017FE2846DCF7F4FAF1 +:104D4000044606B962B61CB1FFF753FF20467FE761 +:104D500000207DE710B5044600F034F800B10120D2 +:104D60002070002010BD244908600020704770B5F5 +:104D70000C4622490D682149214E08310E60102849 +:104D800007D011280CD012280FD0132811D00120E1 +:104D900013E0D4E90001FFF748FF354620600DE03D +:104DA000FFF727FF0025206008E02068FFF7D2FF0B +:104DB00003E0114920680860002020600F48001DB2 +:104DC000056070BD07480A490068884201D101208A +:104DD0007047002070470000C80100200CED00E083 +:104DE0000400FA055C0000204814002000000020A8 +:104DF000BEBAFECA50640200040000201005024042 +:104E0000010000017D49C0B20860704700B57C49CF +:104E1000012808BF03200CD0022808BF042008D0B6 +:104E2000042808BF062004D0082816BFFFDF05208D +:104E300000BD086000BD70B505460C46164610461C +:104E4000F3F7D2F8022C08BF4FF47A7105D0012C89 +:104E50000CBF4FF4C86140F6340144183046F3F7F4 +:104E60003CF9204449F6797108444FF47A71B0FB5B +:104E7000F1F0281A70BD70B505460C460846F4F7E7 +:104E80002FFA022C08BF40F24C4105D0012C0CBF78 +:104E900040F634014FF4AF5149F6CA62511A084442 +:104EA0004FF47A7100F2E140B0FBF1F0281A801E55 +:104EB00070BD70B5064615460C460846F4F710FA64 +:104EC000022D08BF4FF47A7105D0012D0CBF4FF4AD +:104ED000C86140F63401022C08BF40F24C4205D0B4 +:104EE000012C0CBF40F634024FF4AF52891A08442B +:104EF00049F6FC6108444FF47A71B0FBF1F0301AC6 +:104F000070BD70B504460E460846F3F76DF80546C9 +:104F10003046F3F7E2F828444AF2AB3108444FF444 +:104F20007A71B0FBF1F0201A801E70BD2DE9F041BE +:104F300007461E460D4614461046082A16BF04288A +:104F40004EF62830F3F750F807EB4701C1EBC711D5 +:104F500000EBC100022D08BF40F24C4105D0012DED +:104F60000CBF40F634014FF4AF5147182846F4F710 +:104F7000B7F9381A4FF47A7100F6B730B0FBF1F593 +:104F80002046F3F7B9F828443044401DBDE8F081CD +:104F900070B5054614460E460846F3F725F805EBAE +:104FA0004502C2EBC512C0EBC2053046F3F795F8D7 +:104FB0002D1A2046082C16BF04284EF62830F3F789 +:104FC00013F828444FF47A7100F6B730B0FBF1F5CE +:104FD0002046F3F791F82844401D70BD0949082880 +:104FE00018BF0428086803BF20F46C5040F4444004 +:104FF00040F0004020F00040086070470C15004071 +:105000001015004040170040F0B585B00C4605462D +:10501000F9F73EF907466E78204603A96A46EEF78F +:1050200002FD81198EB258B1012F02D0032005B0C4 +:10503000F0BD204604AA0399EEF717FC049D01E099 +:10504000022F0FD1ED1C042E0FD32888BDF80010BD +:10505000001D80B2884201D8864202D14FF0000084 +:10506000E5E702D34FF00200E1E74FF00100DEE791 +:10507000FA48C078FF2814BF0120002070472DE9AE +:10508000F041F74C0746160060680D4603D0F9F76B +:1050900069F8A0B121E0F9F765F8D8B96068F9F7C7 +:1050A00061F8D0B915F00C0F17D06068C17811F015 +:1050B0003F0F1CBF007910F0100F0ED00AE0022E37 +:1050C00008D0E6481FB1807DFF2806D002E0C078F6 +:1050D000FF2802D00120BDE8F0810020BDE8F0816A +:1050E0000A4601460120CAE710B5DC4C1D2200210A +:1050F000A01CE9F7CCF97F206077FF202074E070D6 +:10510000A075A08920F060002030A08100202070D0 +:1051100010BD70B5D249486001200870D248D1490D +:10512000002541600570CD4C1D222946A01CE9F7E1 +:10513000AEF97F206077FF202074E070A075A08911 +:1051400020F060002030A081257070BD2DE9F0476F +:10515000C24C06462078C24F4FF0010907F10808FB +:10516000002520B13878D0B998F80000B8B198F887 +:10517000000068B387F80090D8F804103C2239B3D7 +:105180007570301DE9F759F90520307086F80490E4 +:105190003878002818BF88F8005005D015E03D7019 +:1051A000A11C4FF48E72EAE71D220021A01CE9F732 +:1051B0006EF97F206077FF202074E070A075A089D1 +:1051C00020F060002030A08125700120BDE8F0872C +:1051D0000020BDE8F087A148007800280CBF01201E +:1051E000002070470A460146002048E710B510B17C +:1051F000022810D014E09A4C6068F8F7B3FF78B931 +:105200006068C17811F03F0F1CBF007910F0100FDB +:1052100006D1012010BD9148007B10F0080FF8D195 +:10522000002010BD2DE9FF4F81B08C4D8346DDE994 +:105230000F042978DDF838A09846164600291CBFCF +:1052400005B0BDE8F08F8849097800291CBF05B07A +:10525000BDE8F08FE872B4B1012E08BF012708D075 +:10526000022E08BF022704D0042E16BF082E0327E3 +:10527000FFDFEF7385F81E804FF00008784F8CB188 +:10528000022C1DD020E0012E08BF012708D0022EDD +:1052900008BF022704D0042E16BF082E0327FFDF05 +:1052A000AF73E7E77868F8F75DFF68B97868C178A9 +:1052B00011F03F0F1CBF007910F0100F04D110E067 +:1052C000287B10F0080F0CD14FF003017868F8F735 +:1052D000FDFD30B14178090929740088C0F30B0045 +:1052E0006882CDF800807868F8F73CFF0146012815 +:1052F000BDF8000005F102090CBF40F0010020F0EC +:105300000100ADF8000099F80A2012F0020F4ED10A +:10531000022918BF20F0020049D000BFADF80000FC +:1053200010F0020F04D0002908BF40F0080801D097 +:1053300020F00808ADF800807868C17811F03F0FC0 +:105340001CBF007910F0020F0CD0314622464FF0FE +:105350000100FFF794FE002804BF48F00400ADF8F8 +:10536000000006D099F80A00800860F38208ADF8C2 +:10537000008099F80A004109BDF8000061F3461069 +:10538000ADF8000080B20090BDF80000A8810421B3 +:105390007868F8F79BFD002804BFA88920F060001A +:1053A0000CD0B0F80100C004C00C03D007E040F0FE +:1053B0000200B3E7A88920F060004030A8815CB902 +:1053C00016F00C0F08D07868C17811F03F0F1CBFA1 +:1053D000007910F0100F0DD17868C17811F03F0FEF +:1053E00008D0017911F0400F04D00621F8F76EFDC6 +:1053F00000786877314622460020FFF740FE60BB08 +:105400007968C87810F03F0F3FD0087910F0010F8D +:105410003BD0504605F1040905F10308BAF1FF0F2E +:105420000DD04A464146F8F781FA002808BFFFDF51 +:1054300098F8000040F0020088F8000025E00846D7 +:10544000F8F7DBFC88F800007868F8F7ADFC07286F +:105450000CD249467868F8F7B2FC16E094120020A6 +:10546000CC010020D2120020D40100207868F8F787 +:105470009BFC072809D100217868F8F727FD01680F +:10548000C9F800108088A9F804003146224601209E +:10549000FFF7F5FD80BB7868C17811F03F0F2BD086 +:1054A000017911F0020F27D005F1170605F1160852 +:1054B000BBF1020F18BFBBF1030F08D0F8F774FC63 +:1054C00007280AD231467868F8F787FC12E002987C +:1054D000016831608088B0800CE07868F8F764FC7F +:1054E000072807D101217868F8F7F0FC01683160DE +:1054F0008088B08088F800B0002C04BF05B0BDE8FB +:10550000F08F7868F8F72EFE022804BF05B0BDE8DA +:10551000F08F05F11F047868F8F76DFEAB7AC3F1E0 +:10552000FF01884228BF084605D9A98921F06001FA +:1055300001F14001A981C2B203EB04017868F8F7D8 +:1055400062FEA97A0844A87205B0BDE8F08FB048A1 +:105550000178002918BF704701220270007B10F00B +:10556000080F14BF07200620FCF75FBEA848C17BC8 +:10557000002908BF70470122818921F06001403174 +:1055800081810378002B18BF7047027011F0080F5B +:1055900014BF07200620FCF748BE2DE9FF5F9C4F93 +:1055A000DDF838B0914638780E4600281CBF04B0AC +:1055B000BDE8F09FBC1C1D2200212046E8F767FFD4 +:1055C000944D4FF0010A84F800A06868F8F7ECFBEE +:1055D00018B3012826D0022829D0062818BFFFDFDB +:1055E0002AD000BF04F11D016868F8F726FC20727C +:1055F000484604F1020904F10108FF2821D04A4677 +:105600004146F8F793F9002808BFFFDF98F800003B +:1056100040F0020088F8000031E0608940F013009B +:105620006081DFE7608940F015006081E0E7608914 +:1056300040F010006081D5E7608940F01200608181 +:10564000D0E76868F8F7D9FB88F800006868F8F7D1 +:10565000ABFB072804D249466868F8F7B0FB0EE0B8 +:105660006868F8F7A1FB072809D100216868F8F7F6 +:105670002DFC0168C9F800108088A9F8040084F89E +:1056800009B084F80CA000206073FF20A073A17AF9 +:1056900011F0040F08BF20752AD004F1150804F199 +:1056A0001409022E18BF032E09D06868F8F77CFB96 +:1056B00007280CD241466868F8F78FFB16E000987F +:1056C0000168C8F800108088A8F804000EE0686837 +:1056D000F8F76AFB072809D101216868F8F7F6FB9B +:1056E0000168C8F800108088A8F8040089F80060F4 +:1056F0007F20E0760398207787F800A004B006208A +:10570000BDE8F05FFCF791BD2DE9FF5F424F814698 +:105710009A4638788B4600281CBF04B0BDE8F09F3D +:105720003B48017831B1007B10F0100F04BF04B08A +:10573000BDE8F09F1D227C6800212046E8F7A7FE07 +:1057400048464FF00108661C324D84F8008004F191 +:105750000209FF280BD04A463146F8F7E7F800283F +:1057600008BFFFDF307840F0020030701CE068684E +:10577000F8F743FB30706868F8F716FB072804D287 +:1057800049466868F8F71BFB0EE06868F8F70CFB01 +:10579000072809D100216868F8F798FB0168C9F863 +:1057A00000108088A9F8040004F11D016868F8F76A +:1057B00044FB207284F809A060896BF3000040F07C +:1057C0001A00608184F80C8000206073FF20A073B1 +:1057D00020757F20E0760298207787F8008004B05B +:1057E0000720BDE8F05FFCF720BD094A137C834227 +:1057F00005BF508A88420020012070470448007B82 +:10580000C0F3411002280CBF0120002070470000A7 +:1058100094120020CC010020D4010020C2790D2375 +:1058200041B342BB8188012904D94908818004BF62 +:10583000012282800168012918BF002930D0016847 +:105840006FEA0101C1EBC10202EB011281796FEA3B +:10585000010101EB8103C3EB811111444FEA914235 +:1058600001608188B2FBF1F301FB132181714FF0DC +:10587000010102E01AB14FF00001C1717047818847 +:10588000FF2908D24FF6FF7202EA41018180FF2909 +:1058900084BFFF2282800168012918BF0029CED170 +:1058A0000360CCE7817931B1491E11F0FF018171AC +:1058B0001CBF002070470120704710B50121C17145 +:1058C0008171818004460421F1F7E8FD002818BFAA +:1058D00010BD2068401C206010BD00000B4A022152 +:1058E00011600B490B68002BFCD0084B1B1D186086 +:1058F00008680028FCD00020106008680028FCD050 +:1059000070474FF0805040697047000004E5014047 +:1059100000E4014002000B464FF00000014620D099 +:10592000012A04D0022A04D0032A0DD103E0012069 +:1059300002E0022015E00320072B05D2DFE803F088 +:105940000406080A0C0E100007207047012108E029 +:10595000022106E0032104E0042102E0052100E029 +:105960000621F0F7A4BB0000E24805218170002168 +:10597000017041707047E0490A78012A05D0CA6871 +:105980001044C8604038F1F7B4B88A6810448860A1 +:10599000F8E7002819D00378D849D94A13B1012B68 +:1059A0000ED011E00379012B00D06BB943790BB114 +:1059B000012B09D18368643B8B4205D2C0680EE09D +:1059C0000379012B02D00BB10020704743790BB152 +:1059D000012BF9D1C368643B8B42F5D280689042B9 +:1059E000F2D801207047C44901220A70027972B1CD +:1059F00000220A71427962B104224A7182685232ED +:105A00008A60C068C860BB49022088707047032262 +:105A1000EFE70322F1E770B5B74D04460020287088 +:105A2000207988B100202871607978B10420B14EC6 +:105A30006871A168F068F0F77EF8A860E0685230FD +:105A4000E8600320B07070BD0120ECE70320EEE7B2 +:105A50002DE9F04105460226F0F777FF006800B116 +:105A6000FFDFA44C01273DB12878B8B1012805D04B +:105A7000022811D0032814D027710DE06868C828C7 +:105A800008D30421F1F79BF820B16868FFF773FF92 +:105A9000012603E0002601E000F014F93046BDE8DD +:105AA000F08120780028F7D16868FFF772FF00289E +:105AB000E2D06868017879B1A078042800D0FFDFCF +:105AC00001216868FFF7A7FF8B49E07800F003F930 +:105AD0000028E1D1FFDFDFE7FFF785FF6770DBE735 +:105AE0002DE9F041834C0F46E178884200D0FFDF7A +:105AF00000250126082F7DD2DFE807F0040B2828B7 +:105B00003D434F57A0780328C9D00228C7D0FFDFF4 +:105B1000C5E7A078032802D0022800D0FFDF0420C8 +:105B2000A07025712078B8BB0020FFF724FF7248D1 +:105B30000178012906D08068E06000F0EDF820616E +:105B4000002023E0E078F0F734FCF5E7A0780328A4 +:105B500002D0022800D0FFDF207880BB022F08D0BF +:105B60005FF00500F1F749FBA078032840D0A5704D +:105B700095E70420F6E7A078042800D0FFDF022094 +:105B800004E0A078042800D0FFDF0120A168884746 +:105B9000FFF75EFF054633E003E0A078042800D05D +:105BA000FFDFBDE8F04100F08DB8A078042804D0F4 +:105BB000617809B1022800D0FFDF207818B1BDE874 +:105BC000F04100F08AB8207920B10620F1F715FBEA +:105BD00025710DE0607840B14749E07800F07BF82E +:105BE00000B9FFDF65705AE704E00720F1F705FB15 +:105BF000A67054E7FFDF52E73DB1012D03D0FFDF70 +:105C0000022DF9D14BE70420C0E70320BEE770B5B1 +:105C1000050004D0374CA078052806D101E01020FB +:105C200070BD0820F1F7FFFA08B1112070BD3548AA +:105C3000F0F720FAE070202806D00121F1F7DCF817 +:105C40000020A560A07070BD032070BD294810B56C +:105C5000017809B1112010BD8178052906D00129EC +:105C600006D029B101210170002010BD0F2010BD08 +:105C700000F033F8F8E770B51E4C0546A07808B17F +:105C8000012809D155B12846FFF783FE40B1287895 +:105C900040B1A078012809D00F2070BD102070BD40 +:105CA000072070BD2846FFF79EFE03E0002128462E +:105CB000FFF7B1FE1049E07800F00DF800B9FFDF02 +:105CC000002070BD0B4810B5006900F01DF8BDE85C +:105CD0001040F0F754B9F0F772BC064810B5C07820 +:105CE000F0F723FA00B9FFDF0820F1F786FABDE8E4 +:105CF000104039E6DC010020B41300203D8601008D +:105D0000FF1FA107E15A02000C490A6848F202137A +:105D10009A4302430A607047084A116848F2021326 +:105D200001EA03009943116070470246044B1020BA +:105D30001344FC2B01D8116000207047C8060240B4 +:105D40000018FEBF1EF0040F0CBFEFF30880EFF346 +:105D50000980014A10470000FF7B010001B41EB416 +:105D600000B5F1F76DFC01B40198864601BC01B0A5 +:105D70001EBD00008269034981614FF0010010449B +:105D8000704700005D5D02000FF20C0000F10000A2 +:105D9000694641F8080C20BF70470000FEDF184933 +:105DA0000978F9B90420714608421BD10699154AB1 +:105DB000914217DC0699022914DB02394878DF2862 +:105DC00010D10878FE2807D0FF280BD14FF0010032 +:105DD0004FF000020C4B184741F201000099019A64 +:105DE000094B1847094B002B02D01B68DB6818478A +:105DF0004FF0FF3071464FF00002034B1847000090 +:105E000028ED00E000700200D14B020004000020E9 +:105E1000174818497047FFF7FBFFDBF7CFF900BDC4 +:105E2000154816490968884203D1154A13605B6812 +:105E3000184700BD20BFFDE70F4810490968884298 +:105E400010D1104B18684FF0FF318842F2D080F328 +:105E500008884FF02021884204DD0B4802680321A6 +:105E60000A4302600948804709488047FFDF000075 +:105E7000C8130020C81300200010000000000020FC +:105E8000040000200070020014090040B92F000037 +:105E9000215E0200F0B44046494652465B460FB4CC +:105EA00002A0013001B50648004700BF01BC86468C +:105EB0000FBC8046894692469B46F0BC7047000066 +:105EC0000911000004207146084202D0EFF3098155 +:105ED00001E0EFF30881886902380078102813DBAD +:105EE00020280FDB2C280BDB0A4A12680A4B9A4247 +:105EF00003D1602804DB094A10470220086070477C +:105F0000074A1047074A1047074A12682C3212689E +:105F1000104700005C000020BEBAFECA9B130000C0 +:105F2000554302006F4D0200040000200D4B0E4946 +:105F300008470E4B0C4908470D4B0B4908470D4BC2 +:105F4000094908470C4B084908470C4B06490847C4 +:105F50000B4B054908470B4B034908470A4B0249BD +:105F60000847000041BF000079C10000792D000002 +:105F7000F32B0000812B0000012E0000B71300005E +:105F80003F2900007D2F0000455D020000210160D7 +:105F90004160017270470A6802600B7903717047B3 +:105FA00089970000FF9800005B9A0000C59A0000E6 +:105FB000FF9A0000339B0000659B00009D9B000042 +:105FC0003D9C00007D980000859A0000331200007F +:105FD0000744000053440000B94400004745000056 +:105FE0006146000037470000694700004148000053 +:105FF000DB4800002F490000154A0000354A000028 +:10600000AD160000D1160000F11500004D1600007D +:10601000031700009717000003610000C36200002F +:10602000A1660000BB67000043680000C168000073 +:10603000256900004D6A00001D6B0000896B00009F +:10604000574A00005D4A0000674A0000CF4A00003E +:10605000FB4A0000B74C0000E14C0000194D000065 +:10606000834D00006D4E0000834E00007744000019 +:10607000974E0000B94E0000FF4E000033120000A2 +:10608000331200003312000033120000C12500005B +:1060900047260000632600007F2600000D28000030 +:1060A000A9260000B3260000F526000017270000EF +:1060B000F3270000352800003312000033120000DF +:1060C00097840000B7840000B9840000FD840000BC +:1060D0002B8500001B860000A7860000BB86000001 +:1060E000098700001F880000C1890000E98A0000BC +:1060F0003D740000018B00003312000033120000D9 +:10610000EBB700004DB90000A7B9000021BA0000AC +:10611000CDBA0000010000000000000010011001D5 +:106120003A0200001A020000020004050600000006 +:1061300007111102FFFFFFFF0000FFFFF3B3000094 +:10614000273D0000532100008774000001900000EB +:1061500000000000BF9200009B920000AD92000082 +:10616000000002000000000000020000000000002B +:1061700000010000000000004382000023820000B4 +:10618000918200002D250000EF2400000F25000063 +:10619000DBAA000007AB00000FAD0000FD590000B6 +:1061A000B182000000000000E18200007B250000B9 +:1061B000000000000000000000000000F1AB000043 +:1061C00000000000915A00000300000001555555E1 +:1061D000D6BE898E00006606660C661200000A03B1 +:1061E000AE055208000056044608360CC7FD0000F4 +:1061F0005BFF0000A1FB0000C3FD0000A7A8010099 +:106200009B040100AAAED7AB15412010000000008E +:10621000900A0000900A00007B5700007B570000A6 +:10622000E143000053B200000B7700006320000040 +:10623000BD3A020063BD0100BD570000BD5700001C +:1062400005440000E5B2000093770000D72000006D +:10625000EB3A020079BD0100700170014000380086 +:106260005C0024006801200200000300656C746279 +:10627000000000000000000000000000000000001E +:106280008700000000000000000000000000000087 +:10629000BE83605ADB0B376038A5F5AA9183886C02 +:1062A000010000007746010049550100000000018F +:1062B0000206030405000000070000FB349B5F801A +:1062C000000080001000000000000000000000003E +:1062D000060000000A000000320000007300000009 +:1062E000B4000000F401FA00960064004B00320094 +:1062F0001E0014000A000500020001000049000011 +:1063000000000000D7CF0100E9D1010025D1010034 +:10631000EBCF0100000000008FD40100000101025A +:10632000010202030C0802170D0101020909010113 +:1063300006020918180301010909030305000000FA +:10634000555555252627D6BE898E00002BFB01000A +:1063500003F7010049FA01003FF20100BB220200ED +:10636000B7FB0100F401FA00960064004B00320014 +:106370001E0014000A00050002000100254900006B +:1063800000000000314A0200494A0200614A02004E +:10639000794A0200A94A0200D14A0200FB4A0200DF +:1063A0002F4B02007B470200B7460200A1430200C8 +:1063B0002B5D0200AD730100BD730100E9730100A4 +:1063C000BB740100C3740100D57401002F480200A2 +:1063D000494802001D4802002748020055480200B3 +:1063E0008B480200AB480200C9480200D7480200AF +:1063F000E5480200F54802000D4902002549020067 +:106400003B4902005149020000000000DFBC0000CF +:1064100035BD00004BBD000015590200CD43020000 +:10642000994402000F5C02004D5C0200775C0200A0 +:106430009D710100FD760100674902008D4902004F +:10644000B1490200D74902001C0500402005004068 +:10645000001002007464020008000020E80100003F +:106460004411000098640200F0010020D8110000DF +:10647000A011000001181348140244200B440C061C +:106480004813770B1B2034041ABA0401A40213101A +:08649000327F0B744411C000BF +:02000004000FEB +:1040000000000420CDB20F00F5B20F00F7B20F0090 +:10401000F9B20F00FBB20F00FDB20F00000000006C +:10402000000000000000000000000000C1450F007B +:1040300001B30F000000000003B30F00214D0F007B +:10404000354E0F0007B30F0007B30F0007B30F0083 +:1040500007B30F0007B30F0007B30F0007B30F003C +:1040600007B30F0007B30F0007B30F0007B30F002C +:1040700007B30F0007B30F0007B30F0007B30F001C +:1040800007B30F0085720F0007B30F0007B30F00CF +:1040900041730F0007B30F00814B0F0007B30F00F0 +:1040A00007B30F0007B30F0007B30F0007B30F00EC +:1040B00007B30F0007B30F0000000000000000006E +:1040C00007B30F0007B30F0007B30F0007B30F00CC +:1040D00007B30F0007B30F0007B30F0005850F00EC +:1040E00007B30F0007B30F0007B30F000000000075 +:1040F0000000000007B30F000000000007B30F002E +:1041000000000000000000000000000000000000AF +:10411000000000000000000000000000000000009F +:10412000000000000000000000000000000000008F +:10413000000000000000000000000000000000007F +:10414000000000000000000000000000000000006F +:10415000000000000000000000000000000000005F +:10416000000000000000000000000000000000004F +:10417000000000000000000000000000000000003F +:10418000000000000000000000000000000000002F +:10419000000000000000000000000000000000001F +:1041A000000000000000000000000000000000000F +:1041B00000000000000000000000000000000000FF +:1041C00000000000000000000000000000000000EF +:1041D00000000000000000000000000000000000DF +:1041E00000000000000000000000000000000000CF +:1041F00000000000000000000000000000000000BF +:104200000348044B834202D0034B03B11847704765 +:10421000C8860020C8860020000000000548064926 +:104220000B1AD90F01EBA301491002D0034B03B1C4 +:1042300018477047C8860020C8860020000000008C +:1042400010B5064C237843B9FFF7DAFF044B13B1DE +:104250000448AFF300800123237010BDC8860020FE +:10426000000000005CBD0F0008B5044B1BB1044901 +:104270000448AFF30080BDE80840CFE7000000002D +:10428000CC8600205CBD0F00A3F5803A704700BFCC +:10429000154B002B08BF114B9D46FFF7F5FF002182 +:1042A0008B460F461148124A121A00F075F80C4B53 +:1042B000002B00D098470B4B002B00D098470020D4 +:1042C000002104000D000B4800F016F800F040F843 +:1042D0002000290000F074FA00F014F80000080033 +:1042E000000000000000000000000420C88600203C +:1042F000A4CE002025430F00002301461A4618468D +:1043000000F09CB808B50021044600F0CBF8044B3F +:104310001868C36B03B19847204600F029F900BF25 +:1043200058BB0F0038B5084B084D5B1B9C1007D0DD +:10433000043B1D44013C55F804399847002CF9D141 +:10434000BDE8384007F002BCC8860020C4860020C3 +:1043500070B50D4E0D4D761BB61006D0002455F8E5 +:10436000043B01349847A642F9D1094E094D761B0A +:1043700007F0E6FBB61006D0002455F8043B0134E4 +:104380009847A642F9D170BDBC860020BC860020AB +:10439000C4860020BC860020830730B548D0541E58 +:1043A000002A3FD0CAB2034601E0013C3AD303F8E9 +:1043B000012B9D07F9D1032C2DD9CDB245EA052556 +:1043C0000F2C45EA054536D9A4F1100222F00F0C56 +:1043D00003F1200EE6444FEA121C03F1100242E9F9 +:1043E000045542E9025510327245F8D10CF1010230 +:1043F00014F00C0F03EB021204F00F0C13D0ACF10D +:10440000040323F003030433134442F8045B934290 +:10441000FBD10CF003042CB1CAB21C4403F8012BED +:104420009C42FBD130BD64461346002CF4D1F9E721 +:1044300003461446BFE71A46A446E0E770B4184C9A +:104440002568D5F848411CB365681F2D25DC38B9AF +:10445000AB1C0135656044F82310002070BC704728 +:1044600004EB850C0228CCF88820D4F888614FF042 +:10447000010202FA05F246EA0206C4F88861CCF8A5 +:104480000831E5D1D4F88C311343C4F88C31DFE71F +:1044900005F5A674C5F84841D6E74FF0FF30DDE7D3 +:1044A00058BB0F002DE9F84F2B4B1F68D7F8486118 +:1044B0002DED028BC6B108EE100A8B464FF00108B5 +:1044C0004FF000097468651E0ED4013406EB8404B5 +:1044D000BBF1000F0CD0D4F800315B4508D0013D92 +:1044E0006B1CA4F10404F3D1BDEC028BBDE8F88F82 +:1044F00073682268013BAB420CBF7560C4F8009042 +:10450000002AECD0D6F88801D6F804A008FA05F104 +:1045100001420BD190477268524513D1D7F8483108 +:10452000B342DCD01E46002ECCD1DDE7D6F88C019C +:1045300001420CD1D4F8801018EE100A904772682E +:104540005245EBD0D7F84861002EBBD1CCE7D4F868 +:1045500080009047DFE700BF58BB0F00024B13B14C +:104560000248FFF7C9BE70470000000025430F0056 +:10457000FEE700BF38B50C46E8B90968C9B10F4D70 +:10458000A9420BD06B1A3B2B11D93C22284606F0CE +:10459000EDFE03E0CA5CEA54013BFBD2074800226F +:1045A0003C2103F0D3F90023A887236038BD3D23C5 +:1045B000F2E70E23F9E70123F7E700BF807F002031 +:1045C000074A6FF002039E4502D1EFF3098101E033 +:1045D000EFF308818869A0F102000078104700BF5E +:1045E00075450F0038B50446A8B10D4D00223C2199 +:1045F000284603F0ABF9AB8F83420ED12A4605F172 +:104600003C0152F8040B44F8040B8A42F9D10133FF +:10461000AB87002038BD0E20FCE70B20FAE700BF77 +:10462000807F00200B2970B50446154608462FD917 +:104630002389053304EB43012044431ADAB2012AEB +:1046400026D9814224D8134806F090FE2388522BA5 +:1046500006D1AB0711D062884CF668639A420CD041 +:104660000F2014E034F8022B824204D0AE89964227 +:1046700003F1010308D1002009E0218900230A3455 +:104680004FF6FE704FF440559942EBD80B2070BDA9 +:104690000920FCE7E486002008B5002203F056F963 +:1046A000034B1B88834214BF0B20002008BD00BFB2 +:1046B000E486002038B50C4C21684B1C054612D00E +:1046C0000A484FF4805206F01FFE48B115B1206829 +:1046D00000F00CFC054920684FF4806200F026FCD5 +:1046E0004FF0FF33236038BD30840020F086002077 +:1046F0002DE9F041DFF84480044624F47F65184634 +:10470000D8F8003025F00F05AB420E46174609D009 +:10471000FFF7D0FF0848C8F800504FF480522946F0 +:1047200006F024FE0448C4F30B043A463146204404 +:10473000BDE8F04106F01ABEF0860020308400206B +:10474000BFF34F8F0549064BCA6802F4E06213437A +:10475000CB60BFF34F8F00BFFDE700BF00ED00E06F +:104760000400FA054BDF704710DF704711DF704718 +:1047700013DF704718DF704760DF704769DF7047ED +:1047800061DF70471FB50023CDE90133039368460D +:1047900002230093FFF7EEFF05B05DF804FB08B5B8 +:1047A0004FF0E023D3F8F03DDB0700D500BEFFF764 +:1047B000C7FF0000014B1878704700BFF19600203A +:1047C0002DE9FF484C4B40F60212C3F8402500F09B +:1047D00005FA00F021FE002000F0AAFA00F09CFE8D +:1047E00048B1052000F0A4FA00F0A8FE00F0CCFECD +:1047F000062000F09DFA4FF08043DFF81081D3F8D7 +:104800001C55B12D0CBF0123002388F800304AD07D +:10481000A5F1A8014C424C41384EDFF8F49004F069 +:10482000010333704FF08043D3F8007407F00107A1 +:10483000002C3BD14E2D38D0572D36D0304B1B6835 +:104840001A68304B9A4200D17FBB6D2D2ED01220BA +:1048500000F0B0F9044633789BBB122000F0AAF9AF +:1048600010B1122000F0A6F900F00100307000F045 +:1048700005FDDFF8A0B0824630B12CB9204B1B6893 +:104880001A68214B9A4257D04FF440535A684A4510 +:104890000ABF9B684FF4905303F500731B685B4598 +:1048A0003AD1012448E00124B6E701244FF08043C7 +:1048B00000226D2DC3F81C2500F0E480002CCAD125 +:1048C000C5E70120D0E7544636E03C4634E04FF4DB +:1048D00080030B6071E0022000F02AFAA5F14E037C +:1048E0005842584103F012FEAEE000221146C1E0EA +:1048F00003F05CFEC6E000BF00A00040F19600207F +:1049000034840020D51A5A007E67E54EF2960020C6 +:10491000DBE5B1517CB0EE87002CC2D1BAF1000FBB +:10492000D1D0002FD1D0654B654A1B6865481A600D +:10493000654B43F0010398474FF440535A684A458A +:1049400008BF9B685D4A0CBF03F500734FF490539A +:1049500012681B685B450CBF5C4B002313601CB9DD +:10496000BAF1000F40F08E803378002BB3D00820CE +:1049700000F0DEF998F800300BB9FFF703FF0123D0 +:104980004FF4742088F80030FFF7F2FE504B514985 +:104990001868019001A8FFF7E7FE4F4991F8163318 +:1049A0005A09EC231341DA0707D54C4B9A68002AC1 +:1049B0008DD01A6842F480021A600C22484B029390 +:1049C00000210DEB0200FFF7E7FC40F20113029A11 +:1049D000039303A94020FFF7D1FE0C2200210DEB29 +:1049E0000200FFF7D9FC9DF80C30029A43F0010356 +:1049F00003A9A0208DF80C30FFF7C0FE0C22002187 +:104A00000DEB0200FFF7C8FC01241723029AADF852 +:104A10000E3003A923208DF80C40FFF7AFFE0C22C7 +:104A200000210DEB0200FFF7B7FC0623029A8DF878 +:104A30000C4003A920208DF80E40ADF81030FFF790 +:104A40009DFE02A8FFF798FE4FF4405330785A6855 +:104A50004A450ABF9B684FF4905303F500731B68E7 +:104A60005B4504D0572D02D04E2D7FF43EAF01227E +:104A700040F6B83100F0E8FC3378002B3FF438AF53 +:104A8000FFF774FE00F0EEF800F0F8FBA0B100F0C4 +:104A900043FD88B94FF440535B684B4506D198F805 +:104AA00000300BB9FFF76EFEFFF760FE034B1B688B +:104AB00000221A6000F004FDFFF742FE348400205B +:104AC000D51A5A000048E80160BB0F007E67E54E2A +:104AD0005CBB0F009F470F0000E100E0409D0020FD +:104AE0000080002010B58EB03423ADF802300DF1F7 +:104AF0000201002301A8ADF80430FFF741FE04468F +:104B000040B9BDF80430102B07D0112B0CD001A8F0 +:104B100000F0CCFF20460EB010BD054B01221A70EC +:104B2000072000F005F9F2E7014B18700820F8E7BC +:104B3000F096002013B5002301A80193FFF712FEA1 +:104B4000044660B9019802F053FA019B0A2B09D080 +:104B5000092B09D00B2B02D1012004F09BFB20462E +:104B600002B010BD2046F8E70220F6E708B5FFF7CF +:104B7000B9FF0528FBD1FFF7DDFF0528F7D108BDF8 +:104B80000021024A084602F003BE00BF6D4B0F0031 +:104B90001F2884BF00F01F00044B054A98BF4FF048 +:104BA000A04300F5E07043F8202070470003005058 +:104BB0000C0003001F288ABF064B4FF0A04300F0F3 +:104BC0001F00D3F8103523FA00F0C04300F00100B5 +:104BD000704700BF000300507047000008B54FF059 +:104BE000804301220021DA601220C3F818159A6070 +:104BF000FFF7CEFF1220FFF7CBFF154B4FF4C85045 +:104C000043F001039847FFF7E7FF124A1E210820EF +:104C100002F09AFD08B102F051FE02F0FBFC0E49D1 +:104C20000E4BE02081F823001B684FF47A72B3FB2F +:104C3000F2F3013BB3F1807F08D24FF0E0225361E1 +:104C4000002381F8230093610723136108BD00BF8F +:104C500070BB0F00F496002000ED00E038840020C7 +:104C6000704700004FF0E0224FF40031002310B5F0 +:104C70001361C2F8801102F1C04202F540524FF4B4 +:104C80008031C2F84813C2F80813012151609160C5 +:104C90004FF080420A4CD16002201F2B1A46C6BF3B +:104CA00003F01F0221464FF0A04102F5E0720133EC +:104CB000302B41F82200F0D1FFF7D2FF10BD00BF2A +:104CC00000030050074B23F81010074B002282B05E +:104CD000C3F81021D3F810210192019A01229A60A1 +:104CE00002B07047E898002000C001400A4A0B4B10 +:104CF00011681B68B1FBF3F203FB1211B1EB530F08 +:104D00004FEA530288BF591A4F2359430020B1FB81 +:104D1000F2F189B2FFF7D6BFE4980020F0980020A6 +:104D2000024A136801331360FFF7E0BFE4980020E4 +:104D30001B490A68082823D8DFE800F0130513226E +:104D4000221A1E242A00174B40F6B83018604FF480 +:104D50007F4303F0103323F080539A4218BF0B6057 +:104D60007047104B4FF4967018604FF47F03F0E7D4 +:104D70000C4B64221A6070470A4B40F6B83018603A +:104D80001346E6E7074B40F6B8301860FF23E0E72C +:104D9000044B4FF4967018604FF0FF13D9E700BF33 +:104DA000F4980020F098002000F1804382B01A6847 +:104DB000002A14BF0120002004D000221A601B68C2 +:104DC0000193019B02B070470F4A1378D3B903785F +:104DD0004FF08041C3F34003C1F88035037803F0FE +:104DE0000103C1F87835094B1968C90706D4E021D9 +:104DF00083F800130121C3F88011196001230448CE +:104E000013707047034870470499002000E100E0E8 +:104E10000000AD0B0C00AD0B014B02681A6070472F +:104E2000009900204FF080434FF46072C3F80423D0 +:104E30007047000010B54FF08043D3F80443620779 +:104E400007D54FF48470FFF7AFFF10B11E4B1B68FE +:104E50009847A30608D54FF48A70FFF7A5FF18B14D +:104E60001A4B00201B689847600608D54FF48C70D9 +:104E7000FFF79AFF18B1154B01201B6898472106D0 +:104E800008D54FF48E70FFF78FFF18B1104B00203C +:104E90001B689847E20508D54FF49070FFF784FF30 +:104EA00018B10B4B01201B689847A3050AD54FF496 +:104EB0009270FFF779FF28B1054BBDE810401B68E1 +:104EC0000220184710BD00BFF8980020FC98002071 +:104ED00000990020044AD2F80034DB07FBD50160BA +:104EE000BFF35F8F704700BF00E001404FF0805379 +:104EF0001A69B0FBF2F302FB130373B9084B0222E9 +:104F0000C3F80425C3F80805D3F80024D207FBD55D +:104F100000220448C3F8042570470348704700BFC7 +:104F200000E001400000AD0B0A00AD0BF8B50B4BE3 +:104F30001546012206460F46C3F804250024A54263 +:104F400004D1064B0022C3F80425F8BD57F82410FD +:104F500006EB8400FFF7BEFF0134F0E700E00140FC +:104F6000BFF34F8F0549064BCA6802F4E062134352 +:104F7000CB60BFF34F8F00BFFDE700BF00ED00E047 +:104F80000400FA054FF08053D3F83021082A06D1E7 +:104F9000D3F83431032B02D8024AD05C704700208A +:104FA000704700BF76BB0F0008B54FF08053D3F8B1 +:104FB0003021082A4ED14FF080420021C2F80C1156 +:104FC000C2F81011C2F8381502F54042D3F80414A3 +:104FD000C2F82015D3F80814C2F82415D3F80C141D +:104FE000C2F82815D3F81014C2F82C15D3F81414ED +:104FF000C2F83015D3F81814C2F83415D3F81C14BD +:10500000C2F84015D3F82014C2F84415D3F824147C +:10501000C2F84815D3F82814C2F84C15D3F82C144C +:10502000C2F85015D3F83014C2F85415D3F834141C +:10503000C2F86015D3F83814C2F86415D3F83C14DC +:10504000C2F86815D3F84014C2F86C15D3F844348C +:10505000C2F87035FFF796FF18B1494B494AC3F8BB +:105060008C26FFF78FFF18B1474BFB22C3F818259A +:10507000FFF788FF70B14FF080414FF08053D1F8B7 +:10508000E42ED3F8583222F00F0203F00F0313433B +:10509000C1F8E43EFFF776FF20B13C4B4FF40072BD +:1050A000C3F840264FF08053D3F83031082B09D194 +:1050B0004FF08043D3F80024D10744BF6FF00102C2 +:1050C000C3F80024324AD2F8883043F47003C2F89F +:1050D0008830BFF34F8FBFF36F8F4FF01023D3F89B +:1050E0000C22D2071DD52B4B0122C3F80425D3F87F +:1050F0000024002AFBD04FF01022D2F80C3223F00B +:105100000103C2F80C32234BD3F80024002AFBD051 +:105110000022C3F80425D3F80024002AFBD0FFF7AF +:105120001FFFD3F80022002A03DBD3F80432002B40 +:1051300022DA184B0122C3F80425D3F80024002AF0 +:10514000FBD04FF010221221C2F80012D3F8002435 +:10515000002AFBD04FF010231222C3F804220D4B7B +:10516000D3F80024002AFBD00022C3F80425D3F88A +:105170000024002AFBD0D2E7074B084A1A6008BD7A +:10518000005000404881030000F0004000900240C1 +:1051900000ED00E000E00140388400200090D003E2 +:1051A00013DF704718DF7047064B1878012803D1CA +:1051B000012904BF0221197012B1104602F07EBB12 +:1051C000704700BF3599002008B5FFF7F3FA88B1A2 +:1051D00011481C2101F098FF08B102F06FFB0F4944 +:1051E0000D4800231C2201F07FFF98B1BDE8084064 +:1051F00002F064BB4FF47F20FFF778FE07220749D7 +:105200004FF47F20FFF792FE054B1A78012A04BF66 +:1052100002221A7008BD00BF2C9900203899002086 +:105220003599002070B5124C124D134ED4F800344D +:105230007BB1C4F80056C4F80456C4F80856C4F844 +:105240000C56C4F81056C4F81456C4F81856C4F8CE +:105250001C5602F005FB05F0F4FF20B104F0DAFD66 +:10526000002005F003FA3378023B022BDED870BD34 +:10527000000001403546526E3599002013B54FF4B9 +:105280004053124A596891420CBF9C684FF48054B5 +:1052900001A800F0A7F92368013302D16368013344 +:1052A00011D0019B1A88012A0DD1588820B1996824 +:1052B0000022204602F04AFB019B5B881B1A5842E1 +:1052C000584102B010BD0020FBE700BFDBE5B15143 +:1052D00084B02DE9F34108AC84E80F009DF820402C +:1052E000BDF8228001A80F4616461D4600F07AF947 +:1052F00054B9384B0122FF21A3F802809D601A8027 +:105300009980354B1A7012E0012C17D1314BBA1924 +:105310002A449A60A5221A80FF229A800C9AA3F848 +:105320000280C3E903765D619A612B4B1C70FFF725 +:105330004BFF02B0BDE8F04104B07047032C0FD121 +:10534000019A244B11881980518892689A60C3E9A8 +:105350000376AA2259809A805D611F4B0122D1E712 +:10536000022C15D1019A1B4B1188A5290AD10022C4 +:105370009A60FF221A60FF229A800022C3E903226A +:105380005A61EAE719805188926859809A60F2E779 +:10539000052C0ED1FFF70EFA40B100F097FD08B1D1 +:1053A00002F08CFA0C4B03221A70C2E700F010FADC +:1053B000F5E7042C08D1074B00229A60FF221A60FF +:1053C000019A92889A80B2E7062CB2D1024B04224D +:1053D000EAE700BF389900203599002000B50C4B52 +:1053E0001B7889B063B90B4B1B786BB905238DF81B +:1053F0000C30079B009303AB0FCBFFF769FF03E073 +:1054000004F000FB0028EED009B05DF804FB00BFFB +:1054100034990020289900201FB50023CDE90233DC +:10542000074B019301F030FE30B906494FF47F235A +:1054300001A84B6001F046FE05B05DF804FB00BF1B +:10544000A9510F002C99002070B505460E460AB1EF +:1054500080F00102154B02F001021A7000F0B0FF5B +:10546000044628B935B100F08BFC0446FFF7DAFE9C +:10547000204670BDBEB10E4B0E4A0F481D70294626 +:1054800002F0F8F84FF400444FF4FA7029464FF454 +:105490007A720023E6FB040106F0D0F92A460146A1 +:1054A000064802F0F9F800F06FF9DEE734990020C1 +:1054B00028990020DD530F007CBB0F0008990020C5 +:1054C0001FB5134B4FF0FF32C3F88020C3F8802183 +:1054D0004FF440530F4A596891420DD19C682046C1 +:1054E000FFF75EFE10B14FF000531C60204604B081 +:1054F000BDE8104000F09AB80023CDE902334FF424 +:10550000805406236846CDE90034FFF74BFEE9E7F7 +:1055100000E100E0DBE5B15107B501A800F062F859 +:10552000019B1A88A52A07D09888A0F1AA0358429F +:10553000584103B05DF804FB0120FAE710B501F013 +:105540005DF9A8B10E4B0F4843F00103984701F0F5 +:10555000BFF808B102F0B2F901F050F908B102F059 +:10556000ADF901F001F9044638B102F0A7F904E001 +:1055700001F01EF904460028E4D1204610BD00BF0A +:1055800080BB0F0000A8610000B589B003AB1422F6 +:1055900000211846FEF700FF02228DF80C200022A1 +:1055A00000920FC8FFF794FEFFF73CFE002009B001 +:1055B0005DF804FB13B5044601A800F013F8019B45 +:1055C0001A8822805A8862809A68A2609A88A2808B +:1055D000DA68E2601A6922615A6962619B69A361B3 +:1055E00002B010BD014B0360704700BF00F00F0018 +:1055F000F0B50346186880F308885868FF2464B241 +:10560000EFF30585002D01D1A64600472546064645 +:1056100021273FBAF0B40024002500260027F0B46B +:10562000F92040B2004700BFF0BD00BFFFF7E0BF68 +:1056300073B500230DF1020101A8ADF8023001930A +:1056400002F0C4FDF8B9019C25785DB3174B93F8BF +:105650003020032A28D00C2606FB00F29958E9B91D +:1056600098189D5093F830200132D2B283F8302040 +:10567000BDF802300E4A9B08013B0434436084604D +:10568000084602F085F8019B33B128B1184602F0B4 +:10569000B9FD08B102F012F902B070BD0130042862 +:1056A000DAD1F0E70720EEE70420ECE75499002078 +:1056B000F9560F00084609B102F000B97047000022 +:1056C00010B50C220B4B504319181A5882B193F89D +:1056D00030208C68013AD2B283F8302000221A5070 +:1056E000C1E90122201F02F08DFD08B102F0E6F8A9 +:1056F000002010BD54990020214B70B50122214E8D +:105700001A7096F8303003B970BD1E4C002523681E +:1057100083B1013B042B07D8DFE803F01C0612031A +:105720002800204600F0DEFEE8B2FFF7C9FF08B10E +:1057300002F0C4F80135042D04F10C04E7D1E0E7D0 +:10574000A3686360204600F073FE0028ECD002F0EE +:10575000B5F8E9E7204600F053FF00F02FFF08B14D +:1057600002F0ACF80520FFF7E3FADDE700F074FF84 +:1057700000F092FFBDE870400620FFF7D9BA00BFE5 +:10578000289900205499002008B50E4B002283F878 +:10579000302004210139C3E901221A6003F10C030E +:1057A000F8D1094800F03EFE02F0A8FC08B102F072 +:1057B00085F8064802F08EFC08B102F07FF8002060 +:1057C00008BD00BF54990020B5560F0031560F0098 +:1057D00008B50020FFF774FF0120FFF771FF0220DA +:1057E000FFF76EFF0320FFF76BFFBDE8084002F0F4 +:1057F000CDBC006870476CDF70476DDF70476EDFAF +:1058000070476FDF704772DF704773DF704774DF78 +:10581000704776DF704777DF70477ADF70477CDF4D +:1058200070477FDF704786DF70478FDF704790DFFC +:105830007047AFDF7047B0DF7047B1DF7047B2DF4E +:105840007047B5DF704764DF704766DF70470C282C +:1058500013D8DFE800F01412121212120912071204 +:10586000120D0B0002207047032070470420704780 +:10587000042914BF06200520704706207047012028 +:10588000704702F01BB810B5044608460321FFF725 +:10589000DEFF0246204601F075F918B1BDE8104060 +:1058A00002F00CB810BD00000346032B10B50846EB +:1058B000144620D0042B23D169B1124B18884FF61F +:1058C000FF7398421CD01321FFF7A3FFC0B1BDE8BE +:1058D000104001F0F3BF104602F094F808B101F057 +:1058E000EDFF094B1B689C420AD1012203210748A6 +:1058F00001F048F9EAE70121FFF7A9FF0246F6E7C0 +:1059000010BD00BF3E840020489A0020F899002076 +:10591000F8B50A4DAB889E181D2E14460DDC2F6875 +:10592000FE1802F1010C07F803C07070B01C05F0FE +:105930001DFDAB8802331A19AA80F8BDA899002072 +:10594000F0B54E4E317895B0002940F092804C4C25 +:10595000019110222046FEF71FFD4A4B019923605A +:1059600018220EA8FEF718FD01238DF838302823E1 +:105970001093454B1B78002B7DD0444B04AC03F1B6 +:10598000100518685968224603C20833AB42144612 +:10599000F7D13F4C10220DEB0201E01D05F0B4FCE5 +:1059A000002868D03B4801210460FFF728FF08B1B8 +:1059B00001F084FF384B08AA03F1100C1746186851 +:1059C0005968154603C5083363452A46F7D1206850 +:1059D0000C903248A288A379ADF8342007600122E8 +:1059E00000218DF83630FFF70CFF08B101F066FF9B +:1059F00003238DF84C3004238DF80E3041F23053E0 +:105A0000ADF81030264B08AA9B798DF812300DF1B5 +:105A10000F0104A8FFF717FF012210460DF10E0138 +:105A2000FFF776FF1F4805F04BFE1E49C2B2092062 +:105A3000FFF76EFF102208A90620FFF769FF104943 +:105A400019480EAAFFF7DFFE08B101F037FF164C28 +:105A5000042221780120FFF7DEFE08B101F02EFFBD +:105A600020780121FFF7D1FE08B101F027FF0123C3 +:105A7000337015B0F0BD0623BEE700BF2C9A00209E +:105A8000A899002088990020F4990020A9BB0F0054 +:105A9000B8990020449A0020BF990020289A00203D +:105AA000F899002086BB0F003C840020F0B5044626 +:105AB0000146B1B0A84801F099F82388262B3BD8BD +:105AC0000F2B04D8012B00F0CC8031B0F0BD103B7F +:105AD000162BFAD801A252F823F000BF655B0F0025 +:105AE000735B0F00CB5A0F00A55B0F009F5C0F008C +:105AF000CB5A0F00CB5A0F00CB5A0F00CB5A0F00D6 +:105B0000CB5A0F00BB5C0F00CB5A0F00CB5A0F00D3 +:105B1000CB5A0F00CB5A0F00CB5A0F00CB5A0F00B5 +:105B2000315D0F00CB5A0F00235D0F00CB5A0F00E1 +:105B3000CB5A0F00255C0F00513B9AB2052AC4D8FE +:105B4000052BC2D801A252F823F000BF6F5C0F00F2 +:105B5000BB5C0F00CB5A0F00CB5A0F00475D0F0004 +:105B60000B5C0F007D4BA2881A807D4B00221A70BF +:105B7000ABE78023794CADF824307A4B2088322271 +:105B80001A6010A9012309AAFFF759FE08B101F014 +:105B900095FE754B1B780BB9FFF7D2FE4FF6FF73DE +:105BA000238092E7714B03AC9A79186899888DF835 +:105BB00022200790DA1DADF8201017332646106812 +:105BC0005168254603C508329A422C46F7D1684BE6 +:105BD00009AA03F11807154618685968144603C442 +:105BE0000833BB422246F7D1186820605B48614AFF +:105BF000008810AB8521CDE91456FFF712FE00286E +:105C00003FF463AF01F05AFE5FE7A379002B7FF406 +:105C10005CAF524B13211888FFF7FBFD00283FF4BF +:105C200054AF79E0237A012B7FF44FAF4C4B002225 +:105C30001A704C4B19680139196069B910AB1422FC +:105C40001846FEF7A9FB05228DF84020149A009211 +:105C50000FC8FFF73DFB38E731B0BDE8F040FFF774 +:105C60006FBE3E4B00211888FFF7EFFDD6E7A37902 +:105C7000002B3FF42AAFA27B043A022A3FF625AF5D +:105C8000022B18BF01238DF840304FF4C173ADF8DB +:105C90004430324B10A91888FFF7CDFDAFE7334AE7 +:105CA000258A508D02F118010023854218BF19463C +:105CB0000732A088FFF7B7FDB0E7284B1C884FF6E6 +:105CC000FF75AC4227D02C4B1B78F3B12B49012335 +:105CD00008222046FFF7B1FDF0B902460146022333 +:105CE0002046FFF7AAFDB8B92A460C212046FFF747 +:105CF000A0FDA0F54053023B012B7FF6E6AE08283D +:105D00003FF4E3AE112889D1DFE61A461946204652 +:105D1000FFF793FD82E7082031B0BDE8F04001F0C5 +:105D2000CDBD0E4B002218881146FFF780FD75E7A8 +:105D300000238DF840308DF84130084B10A91888A9 +:105D4000FFF773FDC1E6E188044B1729188828BFC7 +:105D50001721FFF776FD61E7F89900203E840020C7 +:105D60002C9A0020408400203F9A0020B8990020FF +:105D7000D09900203A9A0020F4990020EC99002054 +:105D800030B5464A464800231370464A95B0137012 +:105D900000F048FB01F0F6FD0446002868D14248B7 +:105DA000FEF720FC002866D1404B01221A70112317 +:105DB0003F488DF8043005F083FC3D4982B201A8CC +:105DC000FFF72DFD08B101F079FD0822002104A89C +:105DD000FEF7E2FA0823ADF810301823ADF81230C0 +:105DE0000023ADF8143004A84FF4C873ADF8163092 +:105DF000FFF713FD08B101F061FD00210C2201A89D +:105E0000FEF7CAFA0823ADF804302A4B02932A4859 +:105E10002A4B039301A900F02FFD08B101F04EFDBC +:105E2000274D4022002104A8FEF7B6FA284605F0C7 +:105E300047FCADF810002846059505F041FC079594 +:105E4000204DADF81800284605F03AFC1123ADF8B6 +:105E5000300004A8ADF84C300D9500F0DBFF1A4B74 +:105E600030221A7007225A7010229A70FFF768FDCC +:105E7000204615B030BD04A8FFF7BFFC08B101F003 +:105E80001DFD9DF8113004A801338DF81130FFF786 +:105E9000B2FC00288BD001F011FD88E73F9A00206A +:105EA000A9580F00399A0020B8990020F4990020D1 +:105EB00086BB0F001D5F0F00F899002083580F006C +:105EC0008DBB0F0098BB0F003A9A002010B50F4B06 +:105ED00001221A700E4B18884FF6FF73984207D0B4 +:105EE0001321FFF796FC08B101F0E8FC002010BD7B +:105EF000084C2378002BF9D0074B1878FFF787FC64 +:105F000008B101F0DBFC00232370EFE73F9A00208B +:105F10003E8400202C9A00203C840020F0B50B78B1 +:105F200089B005460C46092B35D8DFE813F02D0063 +:105F3000360041000A001900260007011001440044 +:105F4000150100F089FB0421FFF781FC0246284679 +:105F500000F018FEF8B109B0BDE8F04001F0AEBCA9 +:105F6000FFF7B4FF08B101F0A9FC00F095FB90B178 +:105F700009B0BDE8F04000F09DBBFFF7A7FF002887 +:105F8000F6D001F09BFCF3E7764B01221A704B68C8 +:105F90001A78754B1A7009B0F0BD724B02261E704C +:105FA0004B681B78012BF6D100F008FB3146CBE79C +:105FB0006C4B0322EEE70520FEF7BAFE694B1E7814 +:105FC000022E37D0032E59D0012EE4D104AB10227B +:105FD00018460021FEF7E0F9634A237A12788DF81B +:105FE0001020002203920C2B4FF00302CDE9012078 +:105FF00008D03146284600F0C5FD0028CBD001F07E +:106000005DFCC8E763681846FFF7F3FB0590181DB1 +:10601000FFF7EFFB069003F10800FFF7EAFB07909C +:1060200001A800F005FA0028B5D03146FFF70FFCB3 +:106030000246DFE7237A13F0030011D0C0F1040217 +:106040001A44D2B219464FF0000C0E46013167686F +:10605000C9B2914207F806C0F7D11B1A0433237264 +:106060000123049363680693237A04A89B0805938D +:1060700000F0C6FA00288ED00221D7E7207A8307E5 +:1060800002D03246314662E7384E0190314601F087 +:106090008FFC014618B12846FFF7F5FB7BE76168E6 +:1060A000019A306805F062F9019801F0E7FC0146B9 +:1060B0000028F0D101A9304601F0F0FC014600288B +:1060C000E9D104230493019B9B08059304A833683A +:1060D000069300F007FA074640B9254A237A11686B +:1060E0000B441360234B32681A6054E709281BD114 +:1060F0001F4B217A1A68114419601F4B1B78002B23 +:106100003FF449AF1D4C2388013B9BB22380002BF9 +:106110007FF441AF284600F0FBFC08B101F0CEFB54 +:10612000174B1B88238036E7306801F06BFC014673 +:1061300010B12846FFF7A7FB3946ACE70E4B01220A +:106140001A700F4A8B8813800C4A138023E70A4A7F +:10615000002313700A4AF8E7054B196800F09CFC0D +:10616000F8E600BF399A0020409A00204C9A00209F +:10617000309A0020489A0020389A0020369A002051 +:10618000349A002018DF7047012973B514460D4674 +:106190001A4608D0032912D014B3204602B0BDE835 +:1061A000704001F08BBB0F4B1B78052BF4D10E4BCD +:1061B0001B68002BF0D0214604209847ECE7094EDD +:1061C0003378022BE8D1094B01925B689847064B64 +:1061D00035701B68002BDFD0019A21462846ECE77A +:1061E00002B070BD589A0020509A00205C9A00209E +:1061F00030B589B003AC142200212046FEF7CCF85C +:10620000094B1B88ADF80E30084BDB680693002560 +:10621000079B8DF80C50009394E80F00FFF758F897 +:10622000284609B030BD00BF689A0020B49A00200B +:1062300000B589B003238DF80C300A4B1B88ADF8EC +:106240000E30094B5A6804929A68DB680693079BE4 +:106250000093059203AB0FCBFFF73AF8002009B08B +:106260005DF804FB689A0020B49A002000B589B05C +:1062700001238DF80C300F4B0F4A1B88ADF80E3000 +:106280004FF440535968914208BF9A680B4B5968C4 +:10629000049118BF4FF480529968DB68069305910A +:1062A000009203AB0FCBFFF713F8002009B05DF8A5 +:1062B00004FB00BF689A0020DBE5B151B49A0020CE +:1062C00000B589B003AB142200211846FEF764F82C +:1062D00004228DF80C20002200920FC8FEF7F8FF70 +:1062E00009B05DF804FB0000194BF7B5194C1C60B0 +:1062F000194B02221A70FEF75DFA184B48B1196863 +:10630000204600F001FF00B303B0BDE8F04001F00B +:10631000D5BA1D68124F013D2D0B013504464FF4CF +:1063200040567368BB420CBFB0684FF4805000EB1E +:1063300004300134FEF7DAFDA542F2D80023054807 +:1063400000931A460321FFF71FFF03B0F0BD00BF03 +:10635000CC9A0020C49A0020589A00206C9A002001 +:10636000DBE5B15170B50C4686B00321CDE90210D2 +:106370000546960802A8019304940596FFF702FFCC +:10638000E0B1B4F5805F019B11D8012302A8CDE9EB +:106390000235CDE90446FFF7F5FE78B9032302A8DC +:1063A000CDE90235CDE90446FFF7ECFE06E01A46DA +:1063B000E11AE81AFFF7D6FF0028E6D006B070BD54 +:1063C0001FB5114B0193114B114900241C70114B47 +:1063D00001A81C80CDE9024400F074FE0E4B10B100 +:1063E0001C7004B010BD4FF440520C4954688C42EC +:1063F00007490CBF92684FF480524A60084A002156 +:10640000116001221A70ECE789610F00B09A002038 +:10641000C49A0020689A0020589A0020DBE5B15108 +:10642000549A0020014B1860704700BF509A00201A +:1064300038B54368214C0FCB84E80F002278500711 +:1064400001D5910733D16068830730D1A3689D07D8 +:106450002DD1E16811F0030429D1184408441849EA +:10646000B3F5204F086024D84FF4405315495D68B8 +:106470008D420ABF9B684FF46923C3F56A23984293 +:1064800017D8114B1149196011495960D10709D525 +:10649000104A9A60104B1B78012B0CD1FFF724FF98 +:1064A000204638BD92074CBF0C4A0D4AF1E706243E +:1064B000F6E70C24F4E70824F2E700BFB49A0020C2 +:1064C0006C9A0020DBE5B1515C9A0020E9620F0074 +:1064D000C1620F006D620F00589A002031620F00F8 +:1064E000F1610F002DE9F04385B0002853D0816899 +:1064F00011F0030451D12C4B1B78052B4FD12B4E9F +:1065000042683368DFF8AC9003EB82039500D9F85A +:106510000020934207D94FF0FF3333600C2420460C +:1065200005B0BDE8F0830391FEF744F9DFF88880F9 +:10653000039940B13368D8F800002A4600F0D4FD32 +:10654000D8B10446EBE74FF44053194A586837680E +:10655000904208BF9868039118BF4FF48050002301 +:106560002A463844FEF7C4F80399D8F8000000958D +:106570000B4600220121FFF707FE33681D44D9F8BE +:10658000003035609D420CD1FEF714F90028C6D1C9 +:10659000FEF790F8C3E70E24C1E71024BFE70824F4 +:1065A000BDE70924BBE700BF589A0020549A002099 +:1065B000DBE5B1516C9A0020CC9A002070B50B4BF2 +:1065C0001D6885B90A4E3378042B0CD1094C0A4B4F +:1065D00021781A780948FEF725F810B90523337099 +:1065E00070BD2570FCE70820FAE700BF549A002030 +:1065F000589A0020B09A0020B49A0020709A002087 +:10660000F8B5114B1A78032A03D0042A03D00824C2 +:1066100016E004221A700D4B1C68002CF7D10C4DAB +:1066200043682F789E0007EB8303402B0AD88168CC +:1066300008483246384404F099FE2B7833442B70D6 +:106640002046F8BD0924FBE7589A0020549A002000 +:10665000B09A0020709A002010B50B4C2378052BBF +:1066600010D10A4B0A4A1B68116899420AD10623C5 +:106670002370084B1B685868FEF70EF808B907230B +:10668000237010BD0820FCE7589A00206C9A002067 +:10669000549A0020CC9A0020044B1B78072B02D17F +:1066A000034B9B6818470820704700BF589A00208A +:1066B0005C9A002000B589B006238DF80C30079B4A +:1066C000009303AB0FCBFEF703FE09B05DF804FBAC +:1066D000F0B58DB005A8FEF76DFF089C002C3ED0EC +:1066E0000B9E04F58053B3422DD91E4BA6F5805561 +:1066F00003EA55054FF440539B689C420BD8690050 +:106700002B46A4EB450201F5805106EB4500FFF74F +:1067100029FE0DB0F0BD05F58053012701A8CDE994 +:106720000173CDE90337FFF72DFD0028F1D14FF4B8 +:10673000805301A8CDE9023301970497FFF722FDAA +:106740000028E6D1DBE70123CDE90136A4084FF4A8 +:10675000805301A803930494FFF714FDD9E7204662 +:10676000D7E700BF00F0FFFF00B58DB005A8FEF72A +:1067700021FF099880B1089B8BB94FF440530B4A15 +:10678000596891420ED19B6880080022039001A8AD +:10679000CDE90123FFF7F6FC0DB05DF804FB0B9A81 +:1067A0001344F1E74FF48053EEE700BFDBE5B1514E +:1067B00000B58DB005A8FEF7FDFE099898B1089BBD +:1067C000A3B94FF440530C4A5968914211D19B68C8 +:1067D0000393800803214FF47422049001A8CDE9AB +:1067E0000112FFF7CFFC0DB05DF804FB0B9A1344C8 +:1067F000EEE74FF48053EBE7DBE5B15110B58CB019 +:1068000005A8FEF7D7FE0898B8B10B9C00F5805399 +:10681000A34214D94FF440539B6898421BD80F4BA6 +:10682000A4F5805203EA52035900A0EB430201F59C +:10683000805104EB4300FFF795FD0CB010BD8008BC +:1068400003224FF48053049001A8CDE9012303945F +:10685000FFF798FCF1E70E20EFE700BF00F0FFFF25 +:10686000A8DF7047AADF7047ADDF7047AEDF704723 +:10687000B0DF704762DF70472DE9F0470E4694B0F5 +:106880000546002800F00181002900F0FE804B68D9 +:10689000002B00F0FA804FF6FF7303800023ADF861 +:1068A0000A307B4B04AA03F1100C1746186859688C +:1068B000144603C4083363452246F7D141F23053EE +:1068C0000DF10A013846ADF80830FFF7D3FF044652 +:1068D000002840F0D6802A1D02A90120FFF7C0FF42 +:1068E0000446002840F0CD809DF80A30AB71014687 +:1068F0001C220DA8FDF750FD9DF834300E9443F096 +:1069000004038DF8343001AFAB798DF80E30214699 +:1069100041F2325303223846CDE91044CDE9124406 +:10692000ADF80C30FDF738FD9DF806308DF80440C9 +:1069300023F01F0343F00303214614224FF0110AF2 +:1069400008A88DF806308DF805A00DF10C08FDF7AC +:1069500023FD4FF01409A8880A9405F1080308AA3A +:106960000DA90C94CDE90887ADF82C90FFF77AFFBC +:106970000446002840F0858001461C220DA8FDF742 +:106980000BFD9DF834300E9423F0180343F01803E8 +:106990008DF83430AB798DF80E30214641F2315309 +:1069A00003223846CDE91044CDE91244ADF80C304D +:1069B000FDF7F2FC9DF806308DF8044023F01F032C +:1069C00043F0130321464A4608A88DF806308DF897 +:1069D00005A0FDF7E1FC1723ADF82C30A8880A9438 +:1069E00005F1100308AA0DA90C94CDE90887FFF75B +:1069F00039FF0446002844D101461C220DA8FDF7AA +:106A0000CBFC9DF834300E9443F002038DF8343003 +:106A1000AB798DF80E30214641F2345303223846CB +:106A2000CDE91044CDE91244ADF80C30FDF7B4FCCB +:106A30009DF806308DF8054023F01F0343F0030353 +:106A400021464A4608A88DF806308DF804A0FDF7C7 +:106A5000A3FC02230A93ADF82C30A8880C9605F10C +:106A6000200308AA0DA9CDE90887FFF7FBFE04461D +:106A700038B97368AB62B36803B1EB62054B0122AE +:106A80001A70204614B0BDE8F0870E24F9E700BF65 +:106A9000B9BB0F00D09A002070B5054686B070B320 +:106AA00002884FF6FF739A422BD0174B1B7843B3E3 +:106AB000164C1022080AE170207121FA02F0090E2A +:106AC000072301266071A17102A800216370ADF84F +:106AD00006302270A670FDF75FFC2B8AADF80830F7 +:106AE0000023ADF80C3028888DF80A600DF10603FC +:106AF00002A9CDE90434FFF7B9FE06B070BD0E203F +:106B0000FBE70820F9E700BFD09A0020D19A0020C7 +:106B100030B5044687B060B302884FF6FF739A42DF +:106B200029D0164B1B7833B3154D11232B700B0A4C +:106B30006970AB700B0C090EEB70297105230021F5 +:106B4000102202A8ADF80630FDF726FC238AADF826 +:106B5000083001238DF80A300023ADF80C3020886E +:106B60000DF1060302A9CDE90435FFF77FFE07B05A +:106B700030BD0E20FBE70820F9E700BFD09A0020C7 +:106B8000D19A002030B5044687B038B300884FF65C +:106B9000FF73984224D0134B1B780BB3124D102374 +:106BA00069700321ADF80610AA7000211A4602A8E8 +:106BB0002B70FDF7F1FB238AADF8083001238DF827 +:106BC0000A300023ADF80C3020880DF1060302A92D +:106BD000CDE90435FFF74AFE07B030BD0E20FBE7D4 +:106BE0000820F9E7D09A0020D19A002070B50D4610 +:106BF00088B0044650B149B1826A3AB10B88502B33 +:106C000049D005D8102B43D0112B54D008B070BDFB +:106C1000512BFBD18E79022EF8D10A89038A9A4230 +:106C2000F4D18B7B043B022BF0D99DF816308DF804 +:106C3000106043F001038DF816300B8AADF8183060 +:106C40004B8AADF81A30082201F1140301A8002183 +:106C50000793FDF7A1FBA18A2088019601AACDF830 +:106C600008D0FFF701FE48B3E36A03B1984740F24A +:106C7000FD132088ADF8143004A9FFF7F9FD0028B2 +:106C8000C4D0E36A002BC1D008B0BDE870401847FB +:106C90008B882380BAE7C98803899942B6D1082333 +:106CA0008DF81030123535F8023C8DF81830059506 +:106CB00004A99047AAE74FF6FF73EAE7BDF8003052 +:106CC0002088DB07D3D5002604A9ADF81460FFF7B0 +:106CD000CFFD0028D5D1297D4B1E072B3ED8DFE8FC +:106CE00003F004192226282A3B2C6B8A8DF80460B5 +:106CF000012B05D8062201212046FFF743FFBEE7FE +:106D0000012315358DF80C300295A36A01A92046A0 +:106D100098477BE76A8A01239A428DF80430F0D8BD +:106D200006220221E8E702238DF80430EDE7032371 +:106D3000FAE70423F8E70523F6E76B8A022B02D86B +:106D400003220821D8E7B5F81530ADF80830002B3C +:106D50000CBF07230623E7E70923E5E70322CBE778 +:106D6000A8DF7047AADF70472DE9F04180468EB05A +:106D700015461F460E4611B9084600F09FFD15B98D +:106D8000284600F09BFD1C220DEB02000021FDF7C0 +:106D900003FB9DF81C30ADF80480002443F002038F +:106DA0008DF81C3021460123032268468DF80630F9 +:106DB000CDE90A44CDE90C440894FDF7EDFA3B789F +:106DC0008DF800307B788DF801309DF8023023F08B +:106DD0001F0343F002032146142202A88DF802305B +:106DE000FDF7DAFA0A48CDF80CD001AB029302AAFB +:106DF000149B0088ADF8105007A9ADF81240ADF80B +:106E000014500696FFF7AEFF0EB0BDE8F08100BF4C +:106E1000F89A002030B587B041F60A032B4AADF846 +:106E20000C30044603A901208DF80E00FFF798FFEF +:106E30000546D8B92288E2B922894AB1244B009389 +:106E4000E16804F13C0342F62420FFF78DFFD8B936 +:106E5000228C4AB11F4B0093616A04F13C0342F655 +:106E60002620FFF781FF78B9A36B7BB9284607B0CE +:106E700030BD194B0093616804F13C0342F62920B0 +:106E8000FFF772FF0028D7D00546EFE71A788DF894 +:106E900010205A888DF81120120A8DF812209A8835 +:106EA000DB888DF815301B0A8DF813208DF816300D +:106EB000120A0A4B8DF814200093072204F13C03B8 +:106EC00004A942F65020FFF74FFFDDE7F89A0020B3 +:106ED000E89A0020D89A0020E09A0020F09A00203A +:106EE00029DF704728DF7047064B182202FB00306D +:106EF00000230422C0E90423037183608361C3601B +:106F0000704700BF0C9B002023B502460846C968A5 +:106F100043680093044B53F82150436910F80C1B4D +:106F2000A84702B020BD00BFFC9A002038B5194C1C +:106F30002378182202FB03431A795869012A03D0E7 +:106F4000032A1AD00F2038BD134A996915689A6828 +:106F5000DB68A2EB0532B2F5805F184401EB053126 +:106F600000EB053034BF92084FF48062FFF7B8FFA2 +:106F70000028E8D10123A370E5E74FF080531B6997 +:106F80009BB2B0FBF3F0044B1B681844FFF7AAFF59 +:106F9000EEE700BF0C9B0020049C002070B5134D51 +:106FA0006C780A2C1FD02E783444E4B2092C84BFAC +:106FB0000A3CE4B2182606FB0454A261207103C9FE +:106FC000A360049BE360AB7804F1100282E8030045 +:106FD00023B100206B7801336B7070BDFFF7A6FF03 +:106FE0001128F7D1F5E70420F7E700BF0C9B00203C +:106FF00070B5234CA3782BB100260228A67002D0CE +:10700000032833D070BD25781E4A182101FB0541A5 +:10701000136889680133B1EB033F136014D86378B8 +:107020001660013B63706B1CDBB21821092B01FB5E +:10703000054188BFA5F10903002004312370FFF743 +:1070400063FF2846FFF750FF6378002BDAD0A37860 +:10705000002BD7D1FFF76AFF0028D3D01128D1D059 +:107060002178182303FB0141043105E0217818231E +:1070700003FB014104310D20BDE87040FFF744BF20 +:107080000C9B0020049C002008B50A4B00211960CD +:10709000094B1980997008460131FFF725FF0A292D +:1070A000F9D1064B00201860054BC3E90000C3E985 +:1070B000020008BD049C00200C9B0020009C0020C6 +:1070C000FC9A0020064A03461068042807D008608E +:1070D000411C11601A68034B43F8202000207047C0 +:1070E000009C0020FC9A002013B5CC180C43A40788 +:1070F00008D1009313460A4601460120FFF74EFFD0 +:1071000002B010BD1020FBE707B500220B4600922D +:1071100001460320FFF742FF03B05DF804FB0000C7 +:10712000094B5A7899780132D2B2914208BF0022B5 +:10713000197891421FBF02705878182202FB003064 +:1071400014BF043000207047089C0020082910B5A7 +:10715000044602D0002000F0B1FBD4E90030BDE8C5 +:107160001040184773B5054600240DF107000E4680 +:107170008DF8074000F0B0FB0DF10600FFF7D0FFDF +:1071800090B10670094B9DF8062045605A709DF835 +:10719000070000F0C5FB24B9054B4FF48012C3F87B +:1071A0000021204602B070BD0424F0E7089C0020B6 +:1071B00000E100E0204B21491A682F2300BF00BFE7 +:1071C00000BF00BF00BF00BF00BF00BF8A422FD07A +:1071D00000BF00BF00BF00BF00BF00BF00BF00BFB7 +:1071E00000BF00BF00BF00BF00BF00BF00BF00BFA7 +:1071F00000BF00BF00BF00BF00BF00BF00BF00BF97 +:1072000000BF00BF00BF00BF00BF00BF00BF00BF86 +:1072100000BF00BF00BF00BF00BF00BF00BF00BF76 +:1072200000BF00BF00BF00BF00BF00BF00BF00BF66 +:10723000013BC3D1704700BF388400200024F40014 +:107240000C4B0D484FF4003210B5C3F880200124D8 +:107250004FF48033C0F84833C0F808334460FFF778 +:10726000A9FF064B846000201860FFF7A3FF044BC2 +:10727000187010BD00E100E000100140249D0020C6 +:10728000159D00202DE9F3412549264B0025C1F825 +:107290004051C1F84451C1F84851C1F84C51C1F8AE +:1072A0000051C1F804511B68002B34D0D1F80445BB +:1072B0001D49DFF888800968641A24F07F442F464E +:1072C0001968A14212D81A7CDE69641A0D4462B1B1 +:1072D0005A691F7400929B690193424608216846CF +:1072E00000F056FA08B100F0E9FABEB90F4A104BA7 +:1072F00011781B788B4205D10133DBB2022B08BF1A +:107300000023137012780B4B43F822500A4B4FF4B2 +:107310008012C3F8002102B0BDE8F0813346CFE708 +:1073200000100140289D0020249D0020219D002068 +:10733000209D0020189D002000E100E04D710F000D +:107340002DE9F74FA84AA94913780978A84C994222 +:107350003BD00133DBB2022B08BF00231370A549D9 +:107360001278A54B0F6853F822003B1823F07F4397 +:1073700000220B60236815461646944613B942B1A5 +:10738000236006E0196881420DD902B12360091A11 +:10739000196001262368DFF8689200930027BDB9C1 +:1073A000DFF868A268E0401A0E44D968D3F81CE000 +:1073B000C3F800C031B1BA1922F07F42C3E90121FC +:1073C000DD611D4673460122D8E700252E46E1E720 +:1073D0002846ED69874BD0F804C01B68DFF830E21F +:1073E0008168ACEB030222F07F42724500F2AD806F +:1073F0000A4402600122027422680023C0E90133BA +:10740000C361002A40F0AB802060C8E75A1C9AF89C +:107410000210D4F800B0D2B291428AF8002004BF22 +:1074200000228AF80020182202FB03A31A79986828 +:10743000022A77D0032A00F08580012A1CD190F817 +:1074400010C0BCF1000F17D1D96841601A69826081 +:107450005A69C2609B698361684B1B78002B18BF17 +:1074600061464160B6E7904200F09E809046D26946 +:10747000002AF8D1002303749AF800309AF801200A +:107480009A42C3D1236827B9009A9A4201D1002EAB +:1074900042D0002B00F08580D3F80090584C554B1B +:1074A000D4F804651868574F351A3B7825F07F45A6 +:1074B00003359BB94FF48033C4F84433C4F8043324 +:1074C000514B4FF400324FF00108C3F880211A608D +:1074D000C4F80080FFF76EFE87F80080A9452CBF36 +:1074E0004844401920F07F40C4F84005D4F80435E2 +:1074F0009B1B23F07F43801B033320F07F4083429C +:107500000AD9D4F80435C4F84035FFF753FE3E4B92 +:107510004FF40032C3F80021384B00221A7003B038 +:10752000BDE8F08F5A46D846A2E78BF81020DBF86A +:107530001CB00123BBF1000FF7D1002B9CD0C4F885 +:1075400000B099E700231A46F4E7A3EB0C0323F0FD +:107550007F438B4234BFCB1A002303604AE70168A4 +:10756000136899421BD85B1A1360C2614CE7A1EB08 +:107570000C01D3F81CC01A46BCF1000F0AD06346B8 +:10758000D3F800C08C45F2D3ACEB010CC3F800C0BB +:107590009C4613460160C0F81CC0D861FFE6134644 +:1075A000EEE7FFF74DFEB7E7404510D1DBF81C30A2 +:1075B000236063B9DFF83CE001920121C9F80810AB +:1075C000CEF800300D4B1970FFF7F4FD019A1368E7 +:1075D000D269C8F81C2012B111680B4413602368EB +:1075E0005B4518BF012745E7209D0020219D002015 +:1075F000289D0020249D0020189D0020149D00201F +:1076000000100140159D002000E100E0089C0020D2 +:10761000FEFF7F0008B5FFF713FE104B00200B2282 +:1076200018809A700E4B18600E4B18700E4B187025 +:107630000E4B4FF48012E021C3F8802183F814131D +:107640001A6002F18042A2F56F22C2F8080583F8A1 +:107650001113074BD2F804251A6008BD089C0020BE +:10766000289D0020209D0020219D002000E100E0B9 +:10767000249D0020074B9B784BB132B128B10368A1 +:10768000187C20B959745A61704707207047082048 +:10769000704700BF089C00202DE9F743DFF8848085 +:1076A00098F8023005460E461746ABB3A0B304293E +:1076B00030D9436983B3437C0024012B0DF10700CB +:1076C0000CBF8946A1468DF8074000F005F90DF181 +:1076D0000600FFF725FDD8B1012303700F4B45606D +:1076E000D3F80435C0E90497C0E902369DF80630A6 +:1076F00088F801309DF8070000F012F924B9084B12 +:107700004FF48012C3F80021204603B0BDE8F08397 +:107710000424EFE70724F7E70824F5E70010014009 +:1077200000E100E0089C0020064A92783AB130B1AE +:10773000426922B1002202740221FFF713BD082022 +:10774000704700BF089C00204B1C30B5DB0004468E +:1077500012F003009BB20DD1074D2A601A44074B6B +:107760001A60074B1870074B1870074B1C80074BAB +:10777000198030BD0720FCE7349D0020309D00209B +:107780002C9D00203C9D0020389D00203A9D00202B +:107790002DE9F347DFF8C080B8F800308B42064689 +:1077A0000D4617464CD300240DF107008DF8074015 +:1077B00000F092F8244B254A25481B78008892F85F +:1077C00000C084455FFA8CF138BF4C1CDBB238BF77 +:1077D000E4B2A3422ED014781178CBB2884286BF8F +:1077E0000133DBB2002313709DF8070000F098F816 +:1077F0004FF6FF739C4225D0DFF86090D9F8002047 +:107800004FEAC40A42F8347002EBC403AEB1A5B12A +:10781000104BB8F800001B682A4604FB00303146C4 +:1078200003F0A4FDD9F80030534400209D8002B03D +:10783000BDE8F0874FF6FF74D6E700209880F6E7A2 +:107840000920F4E70420F2E73C9D00202C9D002055 +:107850003A9D0020309D0020389D0020349D00205E +:1078600070B5104C104D22782B789A4200D170BD23 +:107870000E480F4A2378126806880E4802EBC301AF +:10788000006852F83320898803FB060090470A49B4 +:1078900022780988D3B2914286BF0133DBB200233C +:1078A0002370E0E73C9D00202C9D0020389D0020A7 +:1078B000349D0020309D00203A9D00201FB50021FE +:1078C000CDE9021001AA44F20100ADF80410FCF762 +:1078D00066FF05B05DF804FB70B5EFF3108672B675 +:1078E0000C4A946801239CB993600B4B0B4DD3F861 +:1078F000801029401160C3F88050D3F88410516083 +:107900004FF0FF32C3F88420047006B962B670BD30 +:107910000370FAE7409D002000E100E0FC06FFBD97 +:1079200010B5084B9A685AB150B9EFF3108172B68E +:10793000054A1C6814605C685460986001B962B6BE +:1079400010BD00BF409D002000E100E003462AB1C9 +:1079500010881A4619448A4203D170474FF6FF70C7 +:10796000F7E712F8013B40BA80B25840C0F3031366 +:10797000584080EA0033580100F4FF509BB2584051 +:10798000E9E70000064B074A00201870064B1A6012 +:107990000822C3E90120C3E90300C3E905007047D9 +:1079A0004C9D0020509D002030B0002000207047EA +:1079B00030B5F9B1124B5C6800220A60E4B1B0F551 +:1079C000167F1BD8D868013C01305C60D8601C6809 +:1079D00018694FF4177505FB00440C60012101FA8A +:1079E00000F49969013000F00700214318619961A2 +:1079F000104630BD0E20FCE70420FAE70C20F8E723 +:107A000030B00020F0B51C498A689AB34D690E6801 +:107A1000AC1A04F0070423464FF4177707FB036CF6 +:107A2000604511D1012000FA03F58869684088613A +:107A300000204E68D1F818C04FF0010E73440025A5 +:107A4000164403F007030AE0013303F007039D42E5 +:107A5000E4D11020EDE74AB1013A1C4601250EFAA7 +:107A600004F414EA0C0FA6EB0207F4D00DB1C1E93F +:107A70000172F0BD0420FCE730B00020064A136913 +:107A80001268013B4FF4177103F0070301FB032356 +:107A9000C3F858020020704730B0002030B5C0B1A4 +:107AA000B9B10E4BDA68B2B1013ADA609A681C6873 +:107AB00001329A605A694FF4177505FB024404605D +:107AC0000132D4F85802086002F007025A6100201F +:107AD00030BD0E20FCE70420FAE700BF30B00020E4 +:107AE0003FB40C49086890B10B4B1C687CB10B4A41 +:107AF0001568CDE9025000238DF804300B60136047 +:107B000004AB13E90700234604B030BC184704B0A7 +:107B100030BC704754B0002058B0002064B0002042 +:107B2000DC2810B509D0DD2810D0C02816D1FFF709 +:107B3000D7FF0E4B0E4A1A6010BD0E4A0E4B196845 +:107B40001368581C1060C022CA54F2E7094A0A4B55 +:107B500019681368581C1060DB22F5E7064B054ACC +:107B6000196813685C1CC8541460E2E74484002060 +:107B7000917B0F0054B0002064B00020C02802BFE9 +:107B8000014B024A1A60704744840020917B0F0029 +:107B9000C02810B409D0DB280BD0094B094A19685A +:107BA00013685C1CC854146006E05DF8044BFFF7D2 +:107BB00097BF054B054A1A605DF8044B704700BF3C +:107BC00064B0002054B0002044840020217B0F00CA +:107BD00007B501228DF807000DF10701002002F022 +:107BE00085FD00280CBF0420002003B05DF804FBD5 +:107BF00010B5064A064C12682368D05CFFF7E8FF10 +:107C000010B923680133236010BD00BF68B00020A5 +:107C10005CB0002008B5C020FFF7DAFF28B9034B9D +:107C20001B6813B9024B034A1A6008BD5CB0002000 +:107C300048840020F17B0F0008B5DB20FFF7C8FF68 +:107C400010B9024B024A1A6008BD00BF48840020E8 +:107C5000557C0F0010B50C4A0C4C12682368D35C9D +:107C6000C02B03D0DB2B0DD0042010BDDC20FFF790 +:107C7000AFFF0028F9D12368054A01332360054B83 +:107C80001A60F2E7DD20F2E768B000205CB0002067 +:107C9000F17B0F00488400207FB5184C184D194E19 +:107CA000002002F0C3FC30B322689AB1296833681F +:107CB00099420FD2012201A9002002F0C3FC10B9A1 +:107CC0004FF0FF3001E09DF804000F4BC0B21B687D +:107CD0009847E5E70D4B1B686BB10292084A0221F9 +:107CE000126803928DF8041004AA12E9070004B088 +:107CF000BDE87040184704B070BD00BF64B00020FC +:107D000054B0002050B000204484002058B000201F +:107D1000014B18600020704758B00020034B1A78C0 +:107D20000AB901221A700020704700BF4CB0002031 +:107D3000014B0020187070474CB000202DE9F04F27 +:107D400085B0002851D02A4F3B78012B07D0022B59 +:107D500014BF08240424204605B0BDE8F08F254D4B +:107D6000DFF8A090244E254CDFF89C80DFF89CA023 +:107D7000DFF89CB0C9F8001000232B6002233060AC +:107D80003B70C4F800802A68D9F800309A4215D3B5 +:107D9000C4F80080FFF73EFF044608BB184B1B6881 +:107DA00001223A70E3B18DF80420326802922A6809 +:107DB000039204AA12E907009847CCE733682A68BF +:107DC0009A5CC02A03D02A689B5CDB2B04D1236811 +:107DD000534508BFC4F800B023689847042801D170 +:107DE0000024B8E71128CED1FAE71024B3E700BF8A +:107DF0004CB000205CB0002068B000204884002017 +:107E000058B0002060B00020157C0F00F17B0F00FF +:107E1000397C0F00054B064A1860064B1960064B6B +:107E200000201860054B1A60704700BF64B0002046 +:107E30007D7B0F0050B0002054B00020448400200F +:107E4000064B07481B68DB00DBB2002203705B4275 +:107E500042708270C3700421FFF770BF94B000209D +:107E60006CB0002070B52B4C2B4D02462378012BB3 +:107E700014D0022B21D0002B4BD1002A49D1274806 +:107E8000FFF752FC08B1FFF719FD254B1B68002BCB +:107E90003FD0244ABDE8704010781847012A38D1F5 +:107EA0002968214B06311868FFF748FF08B1FFF732 +:107EB00005FD022323700022D8E7022A16D0032AE8 +:107EC0000CD032BB194B15481A6041F67F21FFF7E1 +:107ED000E3FBF0B1BDE87040FFF7F0BC144A136853 +:107EE000013303F0070313600023E3E70F4A13682D +:107EF000052B0AD001331360074B19680A4BBDE804 +:107F0000704018680631FFF719BF064B01221A703E +:107F1000EAE770BDB8B00020ACB0002070B000201F +:107F2000A8B00020B0B00020C0B00020B4B0002045 +:107F300098B00020F0B585B004AB03E907009DF8C8 +:107F40000400032874D8DFE800F00802A3A601208B +:107F500005B0BDE8F040FFF785BF039E544C032EEB +:107F600040F28280029D6B7813F00F0265D00E2ADA +:107F70007AD1042E55D02A78500652D5110650D504 +:107F80001A44AB781A44EB781A4412F0FF0248D135 +:107F9000B71E39462846FFF7D9FCEB5B834240D138 +:107FA00044492A780B6802F00702D8B282422BD1EA +:107FB000013303F007030B60FFF742FF3E4B012242 +:107FC00030461A70FFF75AFD08B1FFF777FC3849C1 +:107FD0004FF41670FFF7ECFC002862D0042802D0A2 +:107FE0000020FFF76BFC35480521FFF713FF08B1B0 +:107FF000FFF764FC324B1B68002B56D04FF000009B +:1080000005B0BDE8F040184720684FF41671FFF73F +:1080100001FF08B1FFF752FC05B0BDE8F040FFF7E3 +:108020000FBF20684FF41671FFF7F4FE00283CD014 +:1080300005B0BDE8F040FFF741BC2978AA780B44B1 +:108040001344EA78134413F0FF030DD11D4A12685C +:108050000132C1F3C20102F00702914204D11A4A6F +:1080600003201370FFF7FEFE25681DB14FF4167153 +:108070002846D9E70E494FF41670FFF799FC60B116 +:10808000042802D02846FFF719FC0C480521CBE74D +:108090000A480521C8E70320CAE720684FF4167193 +:1080A000C2E720684FF416719FE705B0F0BD00BF2E +:1080B000BCB0002094B0002090B000209CB0002004 +:1080C000A4B0002098B00020B0B000200220FFF73C +:1080D000C9BE0000074B10B5044618600648FFF7FC +:1080E00017FE08B1FFF7EAFB002C0CBF0E200020A2 +:1080F00010BD00BFA4B00020357F0F00184A1948FA +:10810000002310B51360184A1360184A1360184A08 +:108110001370184A1370184B184A01211960184B34 +:108120001960184B1970FFF7A5FA08B1032010BDAC +:10813000FFF728FC0028FAD1FFF7F0FD0028F6D160 +:10814000114C4FF416702146FFF732FC0028EDD198 +:1081500020684FF41671BDE81040FFF75BBE00BF0A +:10816000C0B00020CCBB0F00ACB00020B4B00020E9 +:1081700090B00020B8B0002094B00020CD800F0057 +:1081800098B00020B0B00020BCB000200C4A08B568 +:10819000002313600B4A1360FFF708FC08B1FFF7D8 +:1081A0008DFBFFF7C5FD08B1FFF788FB0648FFF719 +:1081B000BBFA042802D10020FFF780FB002008BD95 +:1081C000A8B00020A4B0002070B0002037B50D4644 +:1081D000044698B191B10A4B19780022019259B125 +:1081E00001A91A70FFF75AFC019B063B2B802368FC +:1081F0000433236003B030BD0420FBE70E20F9E711 +:1082000090B000200438FFF7FDBB18DF7047000076 +:10821000F0B51D46154B87B018680F4659681B7A94 +:1082200003AC03C42370124B18685968114B0093B8 +:1082300001AC03C42046164603F042FA214602462A +:10824000384603F093F801A803F03AFA01A9024670 +:10825000304603F08BF8684603F032FA694602466E +:10826000284603F083F807B0F0BD00BFD0BB0F0075 +:10827000D9BB0F00312E30000120704710B51C46CD +:108280000B781E2B0AD000232022052102F07AFC55 +:108290004FF6FF70A04228BF204610BD0020F9E72E +:1082A000F8B5069F14460D463A46002118461E466C +:1082B000FCF772F87CB14FF0E023D3F8F03DDB0718 +:1082C00000D500BE4FF0FF300AE0284600F0F8F974 +:1082D000013504F50074BC4206EB0401F5D32046D9 +:1082E000F8BD0000F8B50A4F0D461E460024069B57 +:1082F0009C4206EB040101D32046F8BD3A462846CD +:1083000000F0B4FA0028F7D0013504F50074EEE768 +:10831000C4B0002030B5264D2A7A8DB09AB107AC92 +:108320001422002120460625FCF736F88DF81C5053 +:108330000B9B009394E80F00FCF7CAFF0620FCF7A4 +:10834000F7FC0DB030BD2B68002BFAD0194B197813 +:1083500019B105201A70FCF7EBFCD5E900329A42FE +:10836000EFD307AC142200212046FCF715F86B7AF6 +:10837000DBB106234FF420424FF474214FF4602008 +:108380008DF81C3002F0C0FF0028D1D0102200214F +:1083900003A8FCF701F84FF460224FF4205303A820 +:1083A000CDE90423FFF731FFC2E78DF81C30BFE7AA +:1083B000C4B000204C840020024B0B604FF40073CB +:1083C0001380704709010100012070470048704781 +:1083D000FA840020044B054A1878054B002814BF86 +:1083E00010461846704700BFE0B20020AF8400205E +:1083F0004D8400202DE9FF411E4B187020B11E4B0B +:108400002A229A720022DA721C4A1D4DDFF878C0C7 +:1084100017461C4BEE4603F110067446186859685F +:10842000F046A8E803000833B342C646F6D12B78DD +:1084300003F00F0310336B4413F8103CD373114B4C +:1084400018685968A646AEE803000833B34274467C +:10845000F6D115F8013B04A901EB1313654513F898 +:10846000103C9373A2F10202D3D100233B7404B0F9 +:10847000BDE8F081E0B20020FA84002064B300205F +:1084800060000010E1BB0F006800001010B570B96B +:10849000134B14481968022202F068FF01230133CC +:1084A000DBB211485B0043F44073038010BD052824 +:1084B00014D80B4B53F82040204603F001F9C3B207 +:1084C0001F2B28BF1F23084A2046E118884202F1CB +:1084D0000202E4D010F8014B1480F7E70020E5E732 +:1084E0000C850020E4B20020E2B200204DDF70478E +:1084F0004EDF70474FDF704750DF704712DF704725 +:1085000000F0C8BE002000F033BD00001FB5244BB2 +:10851000402283F8272300238DF807304FF440537F +:1085200004465A681F4B9A4227D10DF10700FFF706 +:10853000E5FF9DF8073003B30120FFF7D9FF0120C5 +:10854000FFF7D4FF0120FFF7D5FF02A8FFF7D4FF04 +:10855000029BDA0702D5002000F09CFE029B9B07DD +:1085600002D5022000F096FE2046FFF743FF00F000 +:1085700027F802F059FE04B010BD002301A88DF8C1 +:108580000430FCF721FC084B039303A8FCF744FCE0 +:10859000FCF748FC4FF08043D3F838340293D7E718 +:1085A00000E100E0DBE5B15101850F00012000F0A2 +:1085B00071BE0120FCF7BCBB0220FCF7B9BB000078 +:1085C0007FB52F492F4802F0E7FF4FF440532E4A62 +:1085D000596891424CD11A78102A46D9142A186940 +:1085E00044D95B69294CB3FBF4F50A2201A904FBC9 +:1085F000153403F021F92649224802F0CDFF01A9E4 +:10860000204802F0C9FF23491E4802F0C5FF0A2294 +:1086100001A9284603F010F901A91A4802F0BCFF8D +:108620001D49184802F0B8FF4FF47A760A2201A9D2 +:10863000B4FBF6F5284603F0FFF801A9114802F053 +:10864000ABFF15490F4802F0A7FF0A2201A906FB5C +:10865000154003F0F1F801A90A4802F09DFF0F4907 +:10866000084802F099FF04B070BD00200023B9E76C +:108670000B49044804B0BDE8704002F08DBF00BF54 +:10868000FFBB0F0024850020DBE5B15140420F0005 +:108690000CBC0F000ABC0F000EBC0F0019BC0F0071 +:1086A00010BC0F0010B503461C1A944200DB10BD2D +:1086B0000C781CB1013103F8014BF5E72024FAE7EF +:1086C0002DE9F3410C4605464FF400720021204687 +:1086D000FBF762FE6DB95E493E22204602F046FE7F +:1086E000552384F8FE31AA2384F8FF3102B0BDE897 +:1086F000F081B5F5017F2DD8691EB1F5817F24BFCA +:108700006FF4817805EB0801C9B10B02C1EBC151CF +:1087100003F5807004EB412440F693651A1FB2F50F +:10872000696F03F1010206D2AB4214BF91B24FF65A +:10873000FF7124F8131090421346EFD1D6E7F823C7 +:10874000237004F109022346FF2003F8010F93422E +:10875000FBD1DAE7B5F5027F3BD86FF4017C6544C5 +:108760003DB920463B490B22FFF79CFF2823E372CB +:10877000203439492E014FF0000801EB05256FF038 +:108780001907022EB2D80B2229462046FFF78AFF8E +:108790005923212269216374E3746376B31C84F83E +:1087A0000D80A773E1732274A27484F8148084F896 +:1087B0001580A775E17522766383E86830B102F011 +:1087C0007FFFE061013620341035DAE74FF4E9101D +:1087D000F7E7224B9D4289D86FF40277EA19012A04 +:1087E0000FD81D4B03EB0213D9680191084602F024 +:1087F00067FF01990246204602B0BDE8F04102F051 +:10880000B5BD6FF4FD76A9190902B1F5801FBFF45B +:108810006DAF134B236003F1144303F52C1303F6E0 +:10882000023363600F4BC4F8FC314FF46963A361FA +:108830004FF40053A5F20B254FF48072A3600A4B4E +:108840006561E1602261E36104F12000D4E700BFCB +:108850001CBC0F0047BC0F00D4BC0F000801010076 +:108860005546320A306FB10A29009A23F7B5654B95 +:1088700014460A689A420D4639D103F114434A68F6 +:1088800003F52C1303F602339A4230D1D1F8FC21C0 +:108890005D4B9A422BD18B6823F4FF5323F01E03C8 +:1088A0009B049B0CB3F5005F21D10B69B3F5807F6E +:1088B0001DD1C86810F0FF0619D1CB69534A934205 +:1088C00005D0534A934215D0524A93420FD1A0F596 +:1088D0008053B3F5692F07D201234FF4807205F15D +:1088E0002001FBF705FF22E0B0F5805F1FD34FF0BA +:1088F000FF3021E001276772CB68B3F1102F1DD143 +:1089000004223431684602F031FD042205F13801B9 +:108910000DEB020002F02AFD009BB3F5742F03D18A +:10892000019BB3F57E2F01D02772E0E7A772AB69F8 +:10893000002B37D14FF4007003B0F0BDA3F57422C3 +:10894000B2F5204F28D2E27A01F12007DAB9324A93 +:10895000934218D32B69B34215D90422B91968463A +:1089600002F004FD009BD02B14D10422311D0DEB2D +:108970000200394402F0FAFC264B019A9A424FF069 +:1089800001030DD1E372E8682A6901233946A0F595 +:10899000A030A6E70836DDE7B3F5805FC7D3012333 +:1089A0002372A4E72268934207D041F263018B420D +:1089B00000D80AB14FF0FF3323606B6941F26302C4 +:1089C0009342B7D803F0070204EBD303012191408F +:1089D0001A7B1142C8B204D16168024301311A7393 +:1089E0006160D4E900329A42A4D30120FBF762FE11 +:1089F000637A002B9ED0A37A002B9BD10123237294 +:108A000098E700BF5546320A306FB10A4028A5AD3D +:108A10003C8263D629009A2300D80F004FF0805380 +:108A2000D3F83001082802D1D3F8343123B9A0F1AA +:108A30000D0358425841704701207047094B0122ED +:108A400083F8D8200260BFF36F8FBFF34F8F064AC1 +:108A5000904202D0043A904202D1002283F8D820FA +:108A6000704700BF78B300205070024042DF70476B +:108A700043DF704744DF704712DF704710B5134B78 +:108A8000134A5B68C3F3080373B9EFF310835BB950 +:108A900010494B681B0607D58024C1F8844092F822 +:108AA000D8307BB14C60F8E792F8D83033B101464A +:108AB000BDE810400848012201F094B8BDE810401C +:108AC000FFF7BCBFFFF7BAFF4C6010BD00ED00E040 +:108AD00078B3002000E100E07D8A0F000D4B1822E2 +:108AE00002FB0033598A1A8A521A998A92B28A4230 +:108AF00028BF0A46D9681423434303F1804303F592 +:108B00001C33C3F80016C3F80426034B03EB8000A4 +:108B1000FFF7B4BF78B300200470024007B54FF4EC +:108B2000405300205A68084B9A420AD18DF807003A +:108B30000DF10700FFF7A0FF9DF80700003818BFF0 +:108B4000012003B05DF804FBDBE5B15107B5FFF789 +:108B5000E5FF58B1002301A80193FFF78BFF0198AF +:108B6000003818BF012003B05DF804FB4FF08043CC +:108B7000D3F80C0400F00110A0F101135842584141 +:108B8000F1E70000074BD3F8C024D10309D406490C +:108B90000648D1F8C010C3F8A017C3F8A427FFF700 +:108BA0006DBF70470070024078B3002048700240EB +:108BB0000828F0B402D1F0BCFFF7E4BF0F4D104A13 +:108BC000182141436E1800F59473695852F82340F8 +:108BD000F788B388DB1B9BB2A4B2A34228BF23460D +:108BE000142404FB0022C2F80017C2F80437054B16 +:108BF000F0BC03EB8000FFF741BF00BF78B300205B +:108C0000007002402870024070470000014B802233 +:108C10005A60704700E100E0024B8022C3F88420D4 +:108C2000704700BF00E100E0074BD3F80014D3F811 +:108C300000240A43C3F800240022C3F858214FF44B +:108C40008002C3F8042370470070024070B5887832 +:108C5000404D00F07F0318220C26C4095A4306FB3E +:108C600004228E882A44C6F30A061681CA7802F0C6 +:108C70000302012A29D00121374A01FA03F5D4B9A8 +:108C800003F10C06B140C2F80413D2F8141503F531 +:108C900094732943C2F8141542F823402E4BC3F8AD +:108CA000180540F48070C3F80C05BFF36F8FBFF355 +:108CB0004F8F012014E002339940C2F80413D2F818 +:108CC00010352B43C2F81035E8E7082B09D04FF0D8 +:108CD000E023D3F8F00D10F0010001D000BE002019 +:108CE00070BD1D4BDCB9B5F8D42012B18022C3F899 +:108CF0001C250022C3F85021D3F8002312F40012DF +:108D000008BFC3F85421144B4FF44012C3F8042396 +:108D1000D3F8142542F48072C3F81425BEE700226C +:108D2000C3F82C21B5F8C82012B18022C3F81C2545 +:108D3000094BD3F8002312F4001208BFC3F85421E2 +:108D4000064AC3F80423D3F8102542F48072C3F80E +:108D50001025A3E778B3002000700240000820002F +:108D60001D4B2DE9F041802201241C4DDFF8788055 +:108D7000C3F88420274604F10C03A21C07FA02F270 +:108D800007FA03F31343C5F80833A30003F1804344 +:108D900003F51C330026182202FB04805E60314676 +:108DA0009E620134FBF7F8FA082C4FF01802E2D16A +:108DB0000B4BC5F808330B48C5F81C6531466E628D +:108DC000AE64FBF7E9FA044BC5F814758022C5F8C8 +:108DD00010755A60BDE8F08100E100E000700240CB +:108DE0000008300038B4002078B30020F7B51F46E3 +:108DF00001F07F0018231F4CCD090E4643430C2180 +:108E000001FB053104EB010C62500022ACF8047048 +:108E1000ACF8062018B1F5B1FFF760FE18E017BBFB +:108E2000154BD3F88034C3F3C0139D421BD01348B5 +:108E3000FFF724FE124B5B68C3F30803003B18BF27 +:108E4000012300933A463B463146384600F0B5FED2 +:108E5000012003B0F0BD1C44A37A002BF8D0A5720A +:108E6000FFF7A6FEF4E7002DD6D10648FFF706FE71 +:108E7000EEE700BF78B3002000700240507002405F +:108E800000ED00E04C70024011F07F0008B507D102 +:108E90000D4B01225A65BFF36F8FBFF34F8F08BD93 +:108EA0000828F8D0084B41F48072C909C3F8182586 +:108EB000F1D1064B182202FB00339A7A002AEAD03D +:108EC0009972FFF775FEE6E70070024078B3002064 +:108ED00011F0770F12D00A4B41F48072C3F80C15D1 +:108EE000C3F80C25CA09C3F8181504BF01F594711D +:108EF00043F82120BFF36F8FBFF34F8F704700BF40 +:108F000000700240174B0122002110B5C3F8142550 +:108F1000C3F810250A468B0003F1804303F51C3388 +:108F2000013108295A609A62F5D10E4B0E4C5A62F3 +:108F30009A64C3F85821D3F80014D3F800240A43E4 +:108F4000C3F80024D3F80023C3F80823074AC3F862 +:108F500004230021DC222046FBF71EFA4023A382D3 +:108F6000238110BD0070024078B300200514C001B9 +:108F70002DE9F04FB24BB34AD3F80013002385B06C +:108F80001C4601201D4621FA03F6F6070BD552F8C0 +:108F9000236046B100FA03F6344342F82350BFF38E +:108FA0006F8FBFF34F8F0133192BECD1E20706D53A +:108FB000FFF7A8FF00210122084600F0D5FD14F4B8 +:108FC000006FA14D08D09E4BD3F8A8369BB2A5F8F0 +:108FD000D230012385F8D730A30221D5984ED6F898 +:108FE000143513F4807302D0FFF7CCFD0123D6F8BB +:108FF0001025D70540F1188195F8D7305BB1B5F849 +:10900000D2200023012185F8D73092B20091184672 +:10901000882100F0D2FD01220321002000F094FD00 +:10902000660228D5864BD3F8006406F4E062F005AA +:10903000C3F8002406D50122C3F82C250421002002 +:1090400000F082FD71050FD57D4B0122C3F8082584 +:109050009A65D3F8002312F4001208BFC3F8542114 +:109060004FF40012C3F80423B20504D501220521F0 +:10907000002000F069FD23022BD5714BD3F880143A +:10908000C9B28DF80810D3F88424D2B28DF8092023 +:10909000D3F888048DF80A00D3F88C048DF80B00FF +:1090A000D3F890048DF80C00D3F894048DF80D00DB +:1090B000D3F898048DF80E00D3F89C348DF80F3057 +:1090C0004F0601D1052A04D0012202A9002000F098 +:1090D0005EFD5E4B23405BB195F8D830002B40F02D +:1090E000AB804FF0E023D3F8F03DDE0700D500BEA3 +:1090F000DFF85481DFF84891DFF858B14746002681 +:109100004FF0140A06F10C0324FA03F3D807F1B266 +:1091100022D50AFB0693082ED3F808273B6853FA9A +:1091200082F33B604FF0180303FB0653D2B2D8889A +:1091300012FA80F080B2D88000F08E803889904298 +:1091400040F08A80DB88BA889BB29A4240F28480E1 +:1091500011B95846FFF792FC0136092E07F118079E +:10916000D0D13B4B2340002B5BD0354BD3F86C94D4 +:10917000C3F86C94BFF36F8FBFF34F8F14F4806408 +:1091800007D0D3F88044D3F880341906C4F3C01450 +:1091900070D54FF0000A2C4F00264FF0180B29FA1B +:1091A00006F3DA07F0B201D4CEB9C4B1244B1422CD +:1091B00002FB0633FA68D3F8083652FA83F2FA60F3 +:1091C0000BFB0652518A89B251FA83F39BB2538248 +:1091D000538A398A9BB299424FD9FFF77FFC0136F7 +:1091E000082E07F11807DAD100241826012704F108 +:1091F000100329FA03F3DB07E0B203D464B9BAF130 +:10920000000F09D006FB0452B8F80410D3889BB2B3 +:1092100099423DD9FFF7CCFC0134082C08F118081D +:10922000E5D105B0BDE8F08F002B7FF4F4AE4FF42C +:109230000013C6F80833EEE6002385F8D83057E768 +:10924000007002400071024078B30020FCFB1F0058 +:10925000000400014C700240182303FB0653DA8817 +:10926000BA80DA8801230093002392B2184600F0F6 +:10927000A4FC71E74FF0010A8DE7528A01230093A5 +:10928000002340F0800192B2184600F096FCA6E759 +:109290009772C1E7012813B5044600F0C380022885 +:1092A00059D0002855D1784BD3F80025002A50D149 +:1092B0004FF48002C3F808234FF40062C3F800247F +:1092C000BFF36F8FBFF34F8FFFF7A8FB60B16F4BFA +:1092D000D3F8001C032269BB49F27531C3F8001CA6 +:1092E000C3F8142DC3F8001C4FF08053D3F830316D +:1092F000082B0CD1654BD3F8001CC022E9B949F208 +:109300007531C3F8001CC3F8142CC3F8001C5E4B65 +:109310000124C3F80045BFF36F8FBFF34F8FFFF7F2 +:1093200015FCB0B9FFF7FAFB50B102B0BDE8104030 +:10933000FFF79CBBC3F8142DD6E7C3F8142CE6E75F +:109340004FF08043C3F80001D3F800210192019A45 +:109350001C6002B010BD4C4CD4F804351BB1FFF7B3 +:10936000F5FB0028F5D1D4F800341B05FBD54FF4EC +:109370000063C4F80034BFF36F8FBFF34F8F4FF01B +:109380008053D3F83031082B0CD1404BD3F8001C5C +:1093900000293FD149F27532C3F8002CC3F8141CE0 +:1093A000C3F8002CFFF73AFB58B1384BD3F8001C38 +:1093B000A1BB49F27532C3F8002CC3F8141DC3F8E1 +:1093C000002C4FF08053D3F83031082B2E4B0AD1AC +:1093D00040F2E372C3F800284022C3F80428BFF328 +:1093E0006F8FBFF34F8F80220121C3F81C25C3F874 +:1093F0000413274BC3F884215A60FFF7A7FB00280A +:10940000FBD0214B0122C3F80425BFF36F8FBFF3BC +:109410004F8F9EE70022C3F8142CC3E70022C3F845 +:10942000142DCEE7184BD3F80025002A91D0002246 +:10943000C3F80425BFF36F8FBFF34F8F144980200B +:10944000C1F88400D3F80013C3F80813C3F800254B +:10945000BFF36F8FBFF34F8FFFF760FB78B1FFF75C +:1094600007FB0C4B5A68C2F30802003A18BF0122EE +:109470000221002002B0BDE8104000F065BB4FF0B3 +:1094800080435C60EDE700BF0070024000E00640F2 +:1094900000E100E000ED00E00A44034690B288429B +:1094A00002D39A89824202D25A89104480B270470C +:1094B00082888A4210B504D884898B1A9BB29C4258 +:1094C00003D243891A44891A8BB2038210BD9308D0 +:1094D00013B501EB8303044699420BD112F003024A +:1094E00006D0002301A8019301F040FF019B2360F7 +:1094F00002B010BD51F8040B2060EDE72DE9F043F8 +:1095000085B01446BDF83050AB4238BF4289A3EB5A +:1095100005091FFA89F938BFA9EB0209828838BF0B +:109520001FFA89F94A4588460746194605D2FFF7CA +:10953000BFFF058AB0F80490ADB2B9F1000F1FD09B +:10954000A14528BFA146BC88AC421DD9FA889DF828 +:1095500034003968661BA9EB04042C44B3B214FB35 +:1095600002F416FB02F60128B6B2A4B202FB051102 +:1095700016D099450BD802FB09F2404601F0F6FEE1 +:10958000484605B0BDE8F0832D1BADB2DCE732469E +:10959000404601F0EBFE3968224608EB0600EDE795 +:1095A000994506D819FB02F292B24046FFF78FFFA9 +:1095B000E6E726F00305ADB22A4640460191FFF7E3 +:1095C00086FF16F0030628D001990D44C6F1040168 +:1095D00089B2A1424FF0000328BF2146641A0393C9 +:1095E00003ABA4B2A8191A4685420CD13B68013ED0 +:1095F000164419448B420BD1039BC8F80030002C51 +:10960000BED02246D1E715F801CB03F801CBEBE73A +:1096100013F8012B06F8012FECE73968EFE713B5D3 +:10962000930800EB8303984209D112F0030204D09F +:109630000B68019301A901F099FE02B010BD0C68FE +:1096400040F8044BEFE770B59A42A2EB03041D46C5 +:1096500038BF4389A4B238BFE41A838838BFA4B2A4 +:10966000A3420E46114602D2FFF722FF848874B14E +:109670008288AA4208D9C2880168304602FB0511D7 +:1096800001F074FE012070BDAD1AADB2F1E72046C5 +:10969000F9E72DE9F0430746B0F80E908588048A73 +:1096A0001646C288007A85B01FFA89F9A4B288BB31 +:1096B000A145A9EB040038BF7C8980B23CBF001BE8 +:1096C00080B2281A80B2864228BF06464C46AC4279 +:1096D00028D2A5EB0408751B386825441FFA88FCBE +:1096E00015FB02F518FB02F8012B1FFA88F8ADB242 +:1096F00002FB040022D0664517D8724301F036FE03 +:10970000324649463846FFF7C7FEF881304605B075 +:10971000BDE8F083AE4221BF761B02FB0611A146D5 +:109720002E46D3E7641BA4B2D1E74246009101F074 +:109730001DFE009938682A464144DFE7664505D892 +:1097400016FB02F292B2FFF76AFFD9E728F0030492 +:10975000A4B22246CDE90001FFF761FF18F003082B +:10976000019929D0C8F104039BB2AB4200980A6862 +:10977000039228BF2B46013CED1A0DF10C0C20443E +:10978000ADB244466246013C1CF801EB00F801EF23 +:1097900014F0FF04F7D13868904408EB0304421E2C +:1097A000A04504D11844002DAAD02A46CBE718F8CA +:1097B00001CB02F801CFF3E73868F4E7B2F5004FC8 +:1097C00010D882805200C38092B29DF8003003729C +:1097D000531E838152420023C38101604281038270 +:1097E0000120704700207047C189028A89B292B275 +:1097F0009142A1EB020338BF428980889BB23CBFF3 +:109800009B1A9BB2984228BF18467047C289038AA8 +:1098100092B29BB2D31A58425841704710B5C189D1 +:10982000028A848889B292B2914238BF4089A1EB02 +:1098300002039BB23CBF1B1A9BB2E01A80B210BD60 +:1098400038B5C289038A04469BB292B2FFF7FBFE89 +:10985000218A054682B289B22046FFF71DFE20828A +:10986000284638BD73B5C389058A0026ADB20446C3 +:10987000CDE900569BB2FFF741FE218A054602461C +:1098800089B22046FFF708FE2082284602B070BD4C +:1098900038B5C589028AADB292B2AA42A5EB0203DD +:1098A00088BF42899BB288BF9B1A828888BF9BB2BF +:1098B0009A42044614D1007A90B938BD9B1A9BB2E3 +:1098C0009342FBD2E288206802FB030001F04EFDC8 +:1098D000012229462046FFF7DFFDE0810120ECE769 +:1098E0002B46EDE712B10023FFF7D3BE10467047B9 +:1098F0000023C381038283885B009BB25A1E5B42B4 +:10990000828143810120704701720120704700006D +:109910000B4B63B10B4B1B78834206D90A4B1B6878 +:1099200000EB400003EBC0007047C01AC0B2012832 +:1099300003D8064B00EB4000F4E70020704700BF5F +:109940000000000058B4002054B4002004BD0F00F3 +:1099500070B5104E054600242046FFF7D9FF436836 +:109960002846984733780134E4B20133A342F3DA4E +:10997000372200210848FAF70FFD1022FF2107487F +:10998000FAF70AFDBDE8704005481222FF21FAF7F8 +:1099900003BD00BF58B4002059B400205CB40020BF +:1099A0006CB4002037B50C460546C868019200F03B +:1099B000A5FDE368019A0021284603B0BDE83040C8 +:1099C000184773B5054614463AB90378012B04D1FC +:1099D00010460191FFF720F90199281DFFF758FF64 +:1099E00006462CB92B78012B02D12046FFF70EF941 +:1099F00036B94FF0E023D3F8F03DDB0700D500BEC9 +:109A000002B070BD024B5878003818BF0120704773 +:109A100059B40020024B1878C0F38000704700BF93 +:109A200059B40020014B1878704700BF90B4002053 +:109A3000F8B5164E3178054629BB154C1548372226 +:109A4000FAF7AAFC201DFFF753FF134B1C60134BC2 +:109A500023B11348AFF30080124B1860104F00245D +:109A60002046FFF755FF036898473B780134E4B27E +:109A70000133A342F4DA2846FFF7C6F82846FFF779 +:109A8000C5F8012333700120F8BD00BF90B4002059 +:109A9000A486002059B4002094B4002000000000E7 +:109AA00058B4002054B400201FB54378023B0A4646 +:109AB000032B12D8DFE803F0022A1921204B197872 +:109AC0006FF300011970197800246FF341011970C8 +:109AD0005C70197864F3820119701A4B014618689A +:109AE00004B0BDE81040FFF76CBF154B1978C907EB +:109AF00023D5197841F00401EEE711490B78DC0712 +:109B00001BD50B786FF382030B70E6E70C490B78DB +:109B10005B0712D50B786FF382030B700023CDE93E +:109B20000133039303788DF8043005238DF8053055 +:109B3000044B01A91868FFF744FF04B010BD00BF33 +:109B400059B4002094B400201FB50023CDE901339F +:109B50008DF804008DF8051001A811460393FFF756 +:109B6000A3FF05B05DF804FB1FB50023CDE9013369 +:109B700003938DF8040001238DF8081001A8114605 +:109B80008DF80530FFF790FF05B05DF804FB1FB5B9 +:109B9000144600230822CDE9013303938DF8040015 +:109BA00006230DEB02008DF8053001F0DFFB2146A6 +:109BB00001A8FFF779FF04B010BD1FB50024CDE95F +:109BC00001448DF8040007208DF805008DF8081079 +:109BD00001A89DF8181003928DF80930FFF764FF73 +:109BE00004B010BD1FB54FF40063CDE901300391FF +:109BF00001A81146FFF758FF05B05DF804FB00000F +:109C000038B58B7803F07F03082B05460C4608D93E +:109C10004FF0E023D3F8F03DDB0700D500BE002075 +:109C200038BD064B2046997801F00DFB0028EFD097 +:109C300021462846BDE83840FFF708B859B400204F +:109C40002DE9F047DDE9085681460C4690469A46D4 +:109C50000027B84501DC01200EE06378052B04D114 +:109C6000E37803F0030353450AD04FF0E023D3F821 +:109C7000F03DDA0702D40020BDE8F08700BEFAE725 +:109C800021464846FFF7BCFF38B94FF0E023D3F830 +:109C9000F03DDB07EFD500BEEEE7A378DA0914BF8D +:109CA00033702B70237801371C44D2E70B4B01F043 +:109CB0007F0203EB420303EBD1112031487910F00E +:109CC000010008D14B795B0706D44B7943F00403BC +:109CD0004B71012070470020704700BF59B400202D +:109CE0000B4B01F07F0203EB420303EBD111203158 +:109CF0004B7913F0010209D14B79C3F380005B0764 +:109D000005D54B7962F382034B7170470020704791 +:109D100059B4002070B5164D01F07F0605EB4605DD +:109D200005EBD11420346579ED0709D54FF0E02318 +:109D3000D3F8F03DDA0701D4002070BD00BEFBE788 +:109D4000657945F001056571FFF750F80028F4D1F9 +:109D5000637960F300036371637960F38203637175 +:109D60004FF0E023D3F8F03DDB07E5D500BEE4E794 +:109D700059B40020054B01F07F0203EB420303EBD3 +:109D8000D11191F8250000F00100704759B400206E +:109D900010B50B4B01F07F0203EB420303EBD11430 +:109DA000203463799B0709D4FFF76EF8637943F099 +:109DB00002036371637943F00103637110BD00BF57 +:109DC00059B4002010B50B4B01F07F0203EB4203A6 +:109DD00003EBD114203463799B0709D5FFF778F89A +:109DE00063796FF34103637163796FF30003637108 +:109DF00010BD00BF59B40020054B01F07F0203EBFA +:109E0000420303EBD11191F82500C0F340007047E5 +:109E100059B400202DE9F04F87B001F012FA002864 +:109E200000F09182AF4B1D682B78012B02D10020EE +:109E3000FEF7F2FE03A9281DFFF702FD2B78012B88 +:109E4000044602D10020FEF7E1FE002C00F07B82E8 +:109E50009DF80D30013B072B00F2B382DFE813F0D1 +:109E600008001300A8027E028D021F004A02AA0207 +:109E70009DF80C00FFF76CFD00F038FB9A4B9DF845 +:109E800010209A70CEE79DF80C00FFF761FD00F0FE +:109E90002DFB964B002BC5D0FEF78EFBC2E7924CF4 +:109EA0009DF80C50237843F00103237094F825307B +:109EB0006FF3000384F8253094F825306FF38203A4 +:109EC00084F8253094F826306FF3000384F82630A8 +:109ED00094F826306FF3820384F82630002000F0D7 +:109EE0000DFB9DF8106006F06002602A11D14FF062 +:109EF000E023D3F8F03DDC0700D500BE9DF80C0050 +:109F00000021FEF7C1FF9DF80C008021FEF7BCFF89 +:109F100088E7402A0DD176480028EFD000F0EEFA0D +:109F200004AA00212846AFF3008000287FF47AAF0E +:109F3000E4E706F01F06012E00F07181022E00F00A +:109F40009881002ED3D1202A0FD19DF814300F2BE9 +:109F5000D4D82344D878FFF7DBFC01460028CDD0C5 +:109F600004AA2846FFF71EFDDFE7002ABFD19DF8AF +:109F70001130092BBBD801A252F823F009A20F001F +:109F8000F7A10F00EF9E0F00E3A10F00EF9E0F005F +:109F9000A59F0F0021A10F00EF9E0F00BF9F0F0094 +:109FA000D59F0F0004A800F0AFFA9DF812102846C4 +:109FB000FEF73AFE237843F00203237032E763781A +:109FC0008DF80A3001230DF10A0204A9284600F099 +:109FD00059FA27E79DF812906378994537D063784E +:109FE0003BB12846FEF7BCFEA6782846FFF7B0FC3A +:109FF000A670B9F1000F2AD009F1FF30C0B2FEF708 +:10A00000E9F910B14378022B08D04FF0E023D3F8E0 +:10A01000F03DDF077FF56BAF00BE68E7C379C3F3A0 +:10A020008012C3F340131B0143EA4213227822F04B +:10A030003002134323704388C31800F109060093CC +:10A04000009BB3420AD82B4B0BB1FEF7B2FA84F84F +:10A05000019004A9284600F003FAE3E673780B2B7D +:10A0600003BF337896F80380F6184FF00108737831 +:10A07000042BCAD1009B9A1B93B201934FF0000BA3 +:10A080001D4B1B785FFA8BF70133BB42BDDB3846B3 +:10A09000FFF73EFC31468368019A8246284698477E +:10A0A0000828024639D9019B834236D3B8F1010F03 +:10A0B00006D1DAF8083011498B4208BF4FF0020888 +:10A0C0000021CBB298451DD83B4631460C48019241 +:10A0D00001F0E9F8084B019A1B7801339F421644BE +:10A0E000AEDD92E794B4002059B40020B9850F008A +:10A0F00000000000B3850F0058B40020A9A70F008E +:10A100006CB40020B078034454FA83F30131D8785A +:10A11000FF287FF47AAFDF70D3E70BF1010BAFE7D5 +:10A12000BDF81200030A5A1EC0B20E2A3FF6E6AE70 +:10A1300001A151F822F000BF75A10F009FA10F00EF +:10A14000C1A10F00FD9E0F00FD9E0F00D5A10F00C5 +:10A150009FA10F00FD9E0F00FD9E0F00FD9E0F00B2 +:10A16000FD9E0F00FD9E0F00FD9E0F00FD9E0F0047 +:10A1700087A10F00FEF72AF91223024604A92846F8 +:10A1800000F080F9D1E6934B002B3FF4B7AEAFF36C +:10A190000080024600283FF4AAAE4388EEE7022B77 +:10A1A00007D1FEF717F900283FF4A1AE4388024615 +:10A1B000E4E7894B002B3FF4A1AEAFF30080F2E758 +:10A1C000BDF81410FEF762F9024600283FF496AE7F +:10A1D0000378D3E7814B002B3FF490AEAFF30080C0 +:10A1E000F2E7BDF81230012B7FF488AE237843F0FC +:10A1F000080323702DE7BDF81230012B7FF47EAEEB +:10A2000023786FF3C303F4E72378C3F340129B086A +:10A2100003F002031343ADF80A300223D3E69DF89E +:10A2200014300F2B3FF66AAE2344D878FFF770FB4B +:10A23000014600283FF462AE04AA2846FFF7B2FBAD +:10A2400000287FF4EFAD9DF8103013F060047FF428 +:10A2500055AE9DF811300A3B012B3FF64FAE00F092 +:10A260004DF99DF811300A2B7FF4F3AE8DF80A40BA +:10A27000A8E69DF8141001F07F03082B3FF637AED7 +:10A2800004EB430303EBD113D87CFFF741FB0746F4 +:10A290002AB100283FF432AE04AA014661E69DF8D7 +:10A2A000113003F0FD02012A08D0002B7FF41FAE0D +:10A2B0002846FFF7A1FDADF80A00AEE7BDF8122071 +:10A2C00022B9012B284612D1FFF77CFD002F3FF465 +:10A2D000A9AD04AA39462846FFF764FB002000F028 +:10A2E0000DF994F82630DE073FF59CADB1E6FFF797 +:10A2F0004FFDEBE79DF81010394B01F07F0403EBA5 +:10A30000440303EBD11393F825006FF3000083F8A7 +:10A31000250093F825006FF3820083F825003CB9EF +:10A32000059B9DF811209DF80C0000F0FBF879E5E5 +:10A33000D87CFFF7EDFA48B94FF0E023D3F8F03DB1 +:10A34000D80700D500BE07B0BDE8F08F0469059BB3 +:10A350009DF811209DF80C00A04763E5204B1A786A +:10A36000D1077FF55FAD1F4A002A3FF45BAD187837 +:10A37000C0F3C000AFF3008054E5194B1B78DA0737 +:10A380007FF550AD184B002B3FF44CADAFF3008080 +:10A3900048E5FFF7BDFA436913B19DF80C009847F3 +:10A3A0000134124B1B78E0B201338342F1DA39E514 +:10A3B0000024F6E7049B002B3FF434AD0598984742 +:10A3C00030E54FF0E023D3F8F03DDB077FF52AAD11 +:10A3D00000BE27E5000000000000000000000000B3 +:10A3E00059B40020000000000000000058B4002014 +:10A3F00037B514490446CA89888991F90050831AEF +:10A400009BB2402B28BF4023002D10DA904214D07D +:10A410001A4689680C48019300F0A8FF0A4A019B7C +:10A420008021204603B0BDE83040FFF773BC904266 +:10A430004FF0000103D10022F3E78021FBE7024A3D +:10A44000EFE700BF58B500206CB5002011F0800F79 +:10A450004FF000031A460CBF80211946FFF75ABC83 +:10A4600030B4074C05460B4608684968224603C2CB +:10A470000022C4E902222846197830BCFFF7E6BF63 +:10A4800058B50020F8B5184E0C46054608684968CE +:10A49000B260374603C70021F181E1888B4228BFB3 +:10A4A0000B46B381E18889B153B14AB94FF0E0233B +:10A4B000D3F8F03DDA0701D40020F8BD00BEFBE779 +:10A4C0002846FFF795FF30B10120F6E721782846AE +:10A4D000FFF7BCFFF7E74FF0E023D3F8F03DDB07D1 +:10A4E000EAD500BEE9E700BF58B5002002481422B3 +:10A4F0000021F9F751BF00BF58B50020014B18618A +:10A50000704700BF58B5002010B50246044C0068E3 +:10A510005168234603C30023C4E9023310BD00BFC2 +:10A5200058B5002070B52D4C1E462378C909B1EBF3 +:10A53000D31F054618D04EB14FF0E023D3F8F03DBD +:10A54000DA0701D4002070BD00BEFBE7244B13B135 +:10A550002146AFF3008023690BB90120F3E71F4ABE +:10A56000022128469847F8E794F90030002B06DBD3 +:10A57000A0680028E6D01B49324600F0F7FEA2682A +:10A58000E38932443344A260E2889BB29A42E38179 +:10A5900001D03F2E1ED823696BB921782846FFF7DA +:10A5A00055FF0028D9D14FF0E023D3F8F03DDB0769 +:10A5B000C8D500BEC7E70121084A2846984701468A +:10A5C0000028EAD12846FEF75FFC80212846FEF7E6 +:10A5D0005BFCC2E72846FFF70BFFE2E758B5002017 +:10A5E000000000006CB5002070B500F110050446B5 +:10A5F0002846FFF713F93F2817D9E1780020FFF725 +:10A6000055FB90B12846FFF709F93F28E17807D9B3 +:10A6100004F638024023BDE870400020FFF77ABB03 +:10A62000BDE870400020FFF75BBB70BD08B5044B70 +:10A6300040F6B80202FB00301030FFF7D5F808BD35 +:10A64000ACB5002070B540F6B804074E444304F1A1 +:10A65000100092B23044FFF705F905463019FFF7B4 +:10A66000C3FF284670BD00BFACB500202DE9F04106 +:10A670000446FFF7C7F910B90020BDE8F081FFF7E5 +:10A68000C9F906460028F7D140F6B801164D4C43EB +:10A6900004F12408A8444046FFF7A6F80028EBD0B0 +:10A6A0002F193046B978FFF701FB0028E4D004F6F3 +:10A6B00078042544294640224046FFF7D3F8B9786C +:10A6C000044668B103462A463046FFF723FB48B9E3 +:10A6D0004FF0E023D3F8F03DDB07CDD500BECCE74B +:10A6E000FFF7FEFA2046C8E7ACB5002070B50B4C6A +:10A6F00040F6B80303FB0044243492B205462046DA +:10A70000FFF7F0F806462046FFF76EF83F2802D91B +:10A710002846FFF7ABFF304670BD00BFACB5002048 +:10A7200037B5144C40F6B80200212046F9F734FE44 +:10A73000FF234FF4424201256371E2800023082287 +:10A7400063812273009304F138012B464FF4806239 +:10A7500004F110002581FFF731F800952B464FF4E6 +:10A76000806204F5876104F12400FFF727F803B045 +:10A7700030BD00BFACB5002010B50A4C0021052249 +:10A780002046F9F709FE04F110002434FFF7B0F871 +:10A790002046FFF7ADF820460121BDE81040FFF745 +:10A7A000B3B800BFACB50020F7B54B79022B064615 +:10A7B00003D00025284603B0F0BD8B79022BF8D1D9 +:10A7C000204FBB787BBB8B783B700C7809250C4401 +:10A7D00003E023781D44ADB21C446378242B1BD1C5 +:10A7E0009542F6D96378042B12D163790A2B0FD1E5 +:10A7F000154B277801930133009302231A46E11980 +:10A800003046FFF71DFA70B10E3517FA85F5ADB277 +:10A810000C48FFF7E9FECDE7052BE3D12146304692 +:10A82000FFF7EEF938B94FF0E023D3F8F03DDB073E +:10A83000BFD500BEBDE7A3787B7023781D44ADB2C1 +:10A840001C44CFE7ACB50020AEB5002070B50B4678 +:10A850001146127802F06002202A45D1234E8A88E0 +:10A860003478944240D14A78203A032A3CD8DFE831 +:10A8700002F00213162F2BB91D4A0723FFF702FE21 +:10A88000012070BD022BFBD11A4B002BF8D01849C8 +:10A890000020AFF30080F3E7002BF1D1ECE713B910 +:10A8A000FFF7DEFDECE7022BEAD14C881248347149 +:10A8B00004F0010585F00101FFF726F80F4B002B8E +:10A8C000DED0C4F3400229460020AFF30080D7E772 +:10A8D000002BE5D0022BD3D1094B002BD0D04988D7 +:10A8E0000020AFF30080CBE70020CAE7ACB5002022 +:10A8F000B2B5002000000000D0B50020000000002C +:10A90000000000002DE9F347374D1C46EB788B42E1 +:10A9100007460E4607D0AB788B4258D1AB78B3428E +:10A9200032D001245CE0A2B205F6380105F1100036 +:10A93000FEF7D8FF2D4B2BB92D4BEBB92A48FFF76B +:10A9400053FEEBE76B79FF2BF6D005F638094FF095 +:10A95000000805F1100AA045EED019F8013B6A790C +:10A960009A4206D15046FEF751FF10B96979AFF30C +:10A97000008008F10108EEE71E48FEF747FF0028B7 +:10A98000DCD1FDF789F9D9E71B4B13B10020AFF3F8 +:10A9900000800020FFF76AFE0028C2D11748FEF7AA +:10A9A00023FF0028BDD1002CBBD014F03F03B8D149 +:10A9B000A97801933846FFF779F9019B04460028EE +:10A9C000AFD0A9781A463846FFF7A4F908E04FF04F +:10A9D000E023D3F8F04D14F0010401D000BE0024B0 +:10A9E000204602B0BDE8F087ACB5002000000000B2 +:10A9F000997C0F00BCB5002000000000D0B50020FD +:10AA000030B4104B02249A6B83F82C10996883F8A9 +:10AA1000304093F83C408A1A9A6224B942F2050504 +:10AA20009D8783F83E4051B14AB11A7BD20930BCB0 +:10AA300014BF93F82E1093F82F10FFF7A9B930BC6C +:10AA4000704700BF64CE002038B5154B154C054645 +:10AA500073B1607BAFF3008050B942F2077384F8A2 +:10AA60003E00A38728460121BDE83840FFF7C8BF54 +:10AA7000A26BA36894F82F109B1AB3F5805F28BFD0 +:10AA80004FF48053084A9BB22846FFF743F930B988 +:10AA90004FF0E023D3F8F03DDB0700D500BE38BD12 +:10AAA0000000000064CE002064BE002073B5234C7B +:10AAB000E28AA36852BA92B2054612B1B3FBF2F22F +:10AAC00092B2A06BD4F81110B0FBF2F61B1AB3F5DA +:10AAD000805F28BF4FF4805309BA009302FB16022F +:10AAE000174B607B3144FDF7DBFB031E0CDA43F2AE +:10AAF0000333A38701210023284684F83E3002B0A7 +:10AB0000BDE87040FFF77CBF94F82E1006D100938B +:10AB10001A462846FFF751F802B070BD084A9BB2AA +:10AB20002846FFF7F7F80028F6D14FF0E023D3F8D6 +:10AB3000F03DDB07F0D500BEEEE700BF64CE00209D +:10AB400064BE0020C28A836852BA92B223B9002A36 +:10AB50000CBF002002207047C17B282904D1017B53 +:10AB6000C90906D1022070472A2902D1017BC909EF +:10AB7000F8D122B1934234BF022000207047012057 +:10AB800070470000044880F83C1080F83D2080F8B1 +:10AB90003E300120704700BF64CE002002484022B2 +:10ABA0000021F9F7F9BB00BF64CE00200248402223 +:10ABB0000021F9F7F1BB00BF64CE002073B54B79DB +:10ABC000082B054602D0002002B070BD8B79062B01 +:10ABD000F9D1CB79502BF6D1162A07D84FF0E023C4 +:10ABE000D3F8F03DD907EED500BEECE7164C8B78D4 +:10ABF00084F82D3004F12E030E78019304F12F0315 +:10AC0000009302231A463144FFF71AF838B94FF07F +:10AC1000E023D3F8F03DDA07D5D500BED4E7002312 +:10AC200084F8303094F82F101F2322462846FFF76F +:10AC300071F830B94FF0E023D3F8F03DDB0700D5D1 +:10AC400000BE1720C0E700BF64CE00207FB50646D7 +:10AC50001546A1B9137803F07F02022A48D16C7817 +:10AC6000012C45D16A88002A42D1AB883C4D95F829 +:10AC70003020042ADBB204D11946FFF789F80120FD +:10AC80001AE095F82E10994218D1022AF7D1AA6B32 +:10AC9000AB689B1AAB62032385F8303005F12002C4 +:10ACA0000D23FFF737F80028E9D14FF0E023D3F860 +:10ACB000F03DDB0752D500BE04B070BD95F82F10F3 +:10ACC0009942DCD1002ADAD10191FFF753F80199BA +:10ACD0000028D4D13046FFF78FF80028CFD10023C9 +:10ACE00085F830301E4A95F82F101F233046D8E7DC +:10ACF00003F06003202B31D16B78FE2B13D0FF2B98 +:10AD00002CD16B8853BBE9888AB23ABB144B3046CE +:10AD100099872946C3E90D2283F8302083F83E2025 +:10AD2000FFF79EFBABE76B88C3B9EB88012B15D10E +:10AD30008DF80F300B4B1BB1AFF300808DF80F0077 +:10AD40009DF80F3053B1013B8DF80F300DF10F021C +:10AD5000012329463046FFF795FB90E70020ABE73B +:10AD600064CE0020000000002DE9F041AB4C94F8C7 +:10AD70003070012F90B005461E4600F0A181032FD0 +:10AD800000F00D82002F41D194F82F308B4201D07A +:10AD9000012013E01F2E03D12268A14B9A4210D04C +:10ADA00094F82E100423284684F83030FEF7F0FF84 +:10ADB00094F82F102846FEF7EBFF002010B0BDE8F6 +:10ADC000F081984B23626368E67BD4F8088084F8AE +:10ADD0002C70C4E90937012384F8303006F0FD03F4 +:10ADE000282BC4E90D872BD12046FFF7ABFE014687 +:10ADF00018B12846FFF704FE08E0B8F1000F56D05E +:10AE0000282E40F0A8812846FFF750FE94F83030F5 +:10AE1000022BBDD194F82E102846FEF7EDFF002836 +:10AE2000B6D1A368A26B94F82E10934240F2E08151 +:10AE3000207BC00900F0DC812846FEF7A9FFA7E7C8 +:10AE4000B8F1000F17D0237BDB0914D1B8F5805F70 +:10AE500001D90121CDE7744A1FFA88F32846FEF78D +:10AE600059FF0028D2D14FF0E023D3F8F03DDA07A4 +:10AE7000A3D500BEA2E7252E677B08D8192E1AD8C5 +:10AE8000032E00F0F780122E00F09F8096B394F806 +:10AE90003C30002BDDD1A38E634A6449607BFDF713 +:10AEA000EDF9031ED5DB60D1A368002BD1D10223BD +:10AEB00084F83030AAE71A3E0B2EE8D801A353F8E5 +:10AEC00026F000BF35B00F0015AF0F008FAE0F009A +:10AED0008FAE0F008FAE0F008FAE0F008FAE0F0042 +:10AEE0008FAE0F008FAE0F0085AF0F008FAE0F003B +:10AEF0002FAF0F003846FDF7BFF90028D4D194F8E2 +:10AF00003C30002BC3D140F20243A387002384F8D6 +:10AF10003E30BCE7464B002BC6D0E17C3846C1F33F +:10AF2000400301F001020909FDF74EFAE5E70DF1D2 +:10AF3000160206A93846FDF73FFA069B13B1BDF885 +:10AF400016203AB994F83C30002BA0D140F20242CE +:10AF5000A287DCE712BA013B1BBA0892324807937A +:10AF6000082207A900F002FA0823A06800283FF48D +:10AF700070AF834228BF034663632B4A94F82E10B8 +:10AF80009BB26BE70023CDE90733099308238DF8C3 +:10AF90001F300DF11602022306A938468DF8243021 +:10AFA000FDF70AFA069A002ACCD0BDF81630002B1D +:10AFB000C8D012BA5BBA08921B48ADF826300C22F2 +:10AFC00007A900F0D3F90C23CFE72422002107A81A +:10AFD000F9F7E2F980238DF81D30082202232021A1 +:10AFE00009A88DF81E308DF81F30F9F7D5F9102219 +:10AFF00020210BA8F9F7D0F9042220210FA8F9F796 +:10B00000CBF90FAB0BAA09A93846FDF701F90648A1 +:10B01000242207A900F0AAF92423A6E764CE002081 +:10B02000555342435553425364BE002073CE002013 +:10B03000C9830F0003238DF81C3000238DF81D30C9 +:10B040008DF81E308DF81F30704B8BB13846AFF342 +:10B0500000809DF81E3080F0010060F3C7130422C9 +:10B060006B488DF81E3007A900F080F904237CE7B7 +:10B070000120EEE71222002107A8F9F78DF9F0234D +:10B0800094F83C208DF81C300A238DF823304FF0C3 +:10B09000000362F303038DF81E3094F83D308DF801 +:10B0A00028305B4894F83E308DF82930122207A9E9 +:10B0B00000F05CF90023A38784F83E30122354E7A4 +:10B0C000E37BA06B282B06D1636B30449842A063CE +:10B0D000BFF4EDAE97E62A2B41D1E28A52BA92B282 +:10B0E0001AB1A368B3FBF2F292B2D4F81110484F30 +:10B0F000B0FBF2FC09BA02FB1C020096607B3B46E7 +:10B100006144FDF7EFF8002809DAA36B3344A36329 +:10B1100043F20333A387002384F83E3099E6864246 +:10B1200012D9321A40B1A36B03920344391838463E +:10B13000A36300F0B5F9039A94F82F10002300934D +:10B140002846FEF73AFD61E6A36B1E44636BA663D7 +:10B150009E42BFF4ACAE2846FFF776FC56E6237B52 +:10B160003044DB09A0630CD1A38E294A607B04F133 +:10B170000F01FDF783F8002803DA39462846FFF768 +:10B180003FFCD4E90D329A42BFF491AE4FF0E02378 +:10B19000D3F8F03DDB077FF539AE00BE36E694F814 +:10B1A0002E308B427FF432AE0D2E7FF42FAEE37B38 +:10B1B000282B09D02A2B14D0164B53B1607B04F1F5 +:10B1C0000F01AFF3008004E0134B13B1607BAFF3CA +:10B1D0000080002384F83030104A94F82F101F2389 +:10B1E0003CE60F4B002BF4D0607BFDF793F8F0E7C3 +:10B1F0009B1AA362032384F830300A4A0D232846A1 +:10B20000FEF788FD00287FF4C3AD2CE600000000A7 +:10B2100064BE0020000000000000000064CE00209A +:10B2200015830F0084CE002008B50020FEF700FC37 +:10B2300030B94FF0E023D3F8F03DDB0700D500BE76 +:10B2400008BDFEF7EFBB8388C07800F0030002283A +:10B25000C3F30A0315D003281DD001280FD10229FA +:10B2600040F2FF3208BF4FF480629A420FD24FF093 +:10B27000E023D3F8F00D10F0010008D000BE00204C +:10B280007047022904D1B3F5007FF0D10120704747 +:10B29000402BFBD9EBE702290CBF4FF48062402220 +:10B2A0009A42F3D2E3E730B50A44914200D330BD6D +:10B2B0004C78052C06D18C7804F07F0500EB450511 +:10B2C000E4092B550C782144EFE700000649074AB2 +:10B2D000074B9B1A03DD043BC858D050FBDCF9F741 +:10B2E00063FEF8F7D5FF000068BD0F000080002066 +:10B2F000C8860020FEE7FEE7FEE7FEE7FEE7FEE782 +:10B30000FEE7FEE7FEE7FEE7032A70B515D940EA3F +:10B31000010C1CF0030F04460B4621D119462046B0 +:10B320000E680568B54204F1040403F1040317D163 +:10B33000043A032A20461946F0D8541EA2B100F15F +:10B34000FF3C013901E0C3180CD01CF801EF11F8E3 +:10B35000012F9645A4EB0C03F5D0AEEB020070BDB7 +:10B36000541EECE7184670BD104670BD844641EA95 +:10B37000000313F003036DD1403A41D351F8043B6D +:10B3800040F8043B51F8043B40F8043B51F8043BBF +:10B3900040F8043B51F8043B40F8043B51F8043BAF +:10B3A00040F8043B51F8043B40F8043B51F8043B9F +:10B3B00040F8043B51F8043B40F8043B51F8043B8F +:10B3C00040F8043B51F8043B40F8043B51F8043B7F +:10B3D00040F8043B51F8043B40F8043B51F8043B6F +:10B3E00040F8043B51F8043B40F8043B51F8043B5F +:10B3F00040F8043B51F8043B40F8043B403ABDD2CE +:10B40000303211D351F8043B40F8043B51F8043B6F +:10B4100040F8043B51F8043B40F8043B51F8043B2E +:10B4200040F8043B103AEDD20C3205D351F8043BFE +:10B4300040F8043B043AF9D2043208D0D2071CBFCA +:10B4400011F8013B00F8013B01D30B8803806046F3 +:10B45000704700BF082A13D38B078DD010F0030369 +:10B460008AD0C3F10403D21ADB071CBF11F8013BD9 +:10B4700000F8013B80D331F8023B20F8023B7BE728 +:10B48000043AD9D3013A11F8013B00F8013BF9D253 +:10B490000B7803704B7843708B78837060467047ED +:10B4A00088420DD98B1883420AD900EB020CBAB13D +:10B4B000624613F801CD02F801CD9942F9D17047E7 +:10B4C0000F2A0ED8034602F1FF3C4AB10CF1010CE1 +:10B4D000013B8C4411F8012B03F8012F6145F9D190 +:10B4E000704740EA01039B0750D1A2F1100370B5E9 +:10B4F00001F1200C23F00F0501F1100E00F11004F2 +:10B50000AC441B095EF8105C44F8105C5EF80C5CFF +:10B5100044F80C5C5EF8085C44F8085C5EF8045C77 +:10B5200044F8045C0EF1100EE64504F11004E9D174 +:10B53000013312F00C0F01EB031102F00F0400EBCA +:10B54000031327D0043C24F003064FEA940C1E4456 +:10B550001C1F8E465EF8045B44F8045FB442F9D1C8 +:10B560000CF1010402F0030203EB840301EB8401FC +:10B5700002F1FF3C4AB10CF1010C013B8C4411F883 +:10B58000012B03F8012F6145F9D170BD02F1FF3C99 +:10B5900003469BE72246EBE7830710B5044610D12C +:10B5A0000268A2F1013323EA020313F0803F08D1BD +:10B5B00050F8042FA2F1013323EA020313F0803F75 +:10B5C000F6D003781BB110F8013F002BFBD100F03F +:10B5D00003F8204610BD00BF80EA0102844612F045 +:10B5E000030F4FD111F0030F32D14DF8044D11F07C +:10B5F000040F51F8043B0BD0A3F101329A4312F02F +:10B60000803F04BF4CF8043B51F8043B16D100BF07 +:10B6100051F8044BA3F101329A4312F0803FA4F198 +:10B6200001320BD14CF8043BA24312F0803F04BF1F +:10B6300051F8043B4CF8044BEAD023460CF8013B8C +:10B6400013F0FF0F4FEA3323F8D15DF8044B704736 +:10B6500011F0010F06D011F8012B0CF8012B002A74 +:10B6600008BF704711F0020FBFD031F8022B12F063 +:10B67000FF0F16BF2CF8022B8CF8002012F47F4F1E +:10B68000B3D1704711F8012B0CF8012B002AF9D126 +:10B69000704700BF00000000000000000000000034 +:10B6A000000000000000000000000000000000009A +:10B6B000000000000000000000000000000000008A +:10B6C00090F800F06DE9024520F007016FF0000CE2 +:10B6D00010F0070491F820F040F049804FF000048A +:10B6E0006FF00700D1E9002391F840F000F1080065 +:10B6F00082FA4CF2A4FA8CF283FA4CF3A2FA8CF39D +:10B700004BBBD1E9022382FA4CF200F10800A4FA03 +:10B710008CF283FA4CF3A2FA8CF3E3B9D1E9042357 +:10B7200082FA4CF200F10800A4FA8CF283FA4CF38E +:10B73000A2FA8CF37BB9D1E9062301F1200182FA48 +:10B740004CF200F10800A4FA8CF283FA4CF3A2FA4E +:10B750008CF3002BC6D0002A04BF04301A4612BA5C +:10B76000B2FA82F2FDE8024500EBD2007047D1E95F +:10B77000002304F00305C4F100004FEAC50514F0EE +:10B78000040F91F840F00CFA05F562EA05021CBFBF +:10B7900063EA050362464FF00004A9E7F0B5254FC0 +:10B7A000A2F1020E164605460C460FCF8BB0EC46B2 +:10B7B000ACE80F000FCFACE80F0097E803004CF89F +:10B7C000040BBEF1220F8CF800102ED804F1FF3EBE +:10B7D00070464FF0000CB5FBF6F206FB125328330F +:10B7E0006B44614613F828CC00F801CF2B469E42EB +:10B7F00001F1010C1546EED9002304F80C3089B193 +:10B80000A44472461EF8010F1CF8015D8EF800502A +:10B810006FEA0E0302322344121B0B449A428CF847 +:10B820000000EEDB20460BB0F0BD002020700BB016 +:10B83000F0BD00BF34BD0F00FFF7B0BF53B94AB928 +:10B84000002908BF00281CBF4FF0FF314FF0FF3028 +:10B8500000F074B9ADF1080C6DE904CE00F006F803 +:10B86000DDF804E0DDE9022304B070472DE9F0477C +:10B87000089D04468E46002B4DD18A42944669D9D4 +:10B88000B2FA82F252B101FA02F3C2F1200120FAB7 +:10B8900001F10CFA02FC41EA030E94404FEA1C4805 +:10B8A000210CBEFBF8F61FFA8CF708FB16E341EA01 +:10B8B000034306FB07F199420AD91CEB030306F187 +:10B8C000FF3080F01F81994240F21C81023E6344A8 +:10B8D0005B1AA4B2B3FBF8F008FB103344EA03444C +:10B8E00000FB07F7A7420AD91CEB040400F1FF3361 +:10B8F00080F00A81A74240F207816444023840EA9E +:10B900000640E41B00261DB1D4400023C5E90043D6 +:10B910003146BDE8F0878B4209D9002D00F0EF8059 +:10B920000026C5E9000130463146BDE8F087B3FA8C +:10B9300083F6002E4AD18B4202D3824200F2F98074 +:10B94000841A61EB030301209E46002DE0D0C5E977 +:10B95000004EDDE702B9FFDEB2FA82F2002A40F0C3 +:10B960009280A1EB0C014FEA1C471FFA8CFE0126C6 +:10B97000200CB1FBF7F307FB131140EA01410EFB6A +:10B9800003F0884208D91CEB010103F1FF3802D211 +:10B99000884200F2CB804346091AA4B2B1FBF7F00B +:10B9A00007FB101144EA01440EFB00FEA64508D92E +:10B9B0001CEB040400F1FF3102D2A64500F2BB806B +:10B9C0000846A4EB0E0440EA03409CE7C6F12007BA +:10B9D000B34022FA07FC4CEA030C20FA07F401FA00 +:10B9E00006F31C43F9404FEA1C4900FA06F3B1FB89 +:10B9F000F9F8200C1FFA8CFE09FB181140EA0141EE +:10BA000008FB0EF0884202FA06F20BD91CEB01018A +:10BA100008F1FF3A80F08880884240F28580A8F1E2 +:10BA200002086144091AA4B2B1FBF9F009FB101134 +:10BA300044EA014100FB0EFE8E4508D91CEB0101D2 +:10BA400000F1FF346CD28E456AD90238614440EA75 +:10BA50000840A0FB0294A1EB0E01A142C846A646F5 +:10BA600056D353D05DB1B3EB080261EB0E0101FA7E +:10BA700007F722FA06F3F1401F43C5E900710026DB +:10BA80003146BDE8F087C2F12003D8400CFA02FC31 +:10BA900021FA03F3914001434FEA1C471FFA8CFE41 +:10BAA000B3FBF7F007FB10360B0C43EA064300FB31 +:10BAB0000EF69E4204FA02F408D91CEB030300F1CF +:10BAC000FF382FD29E422DD9023863449B1B89B286 +:10BAD000B3FBF7F607FB163341EA034106FB0EF30F +:10BAE0008B4208D91CEB010106F1FF3816D28B42BC +:10BAF00014D9023E6144C91A46EA004638E72E4688 +:10BB0000284605E70646E3E61846F8E64B45A9D27F +:10BB1000B9EB020864EB0C0E0138A3E74646EAE7EE +:10BB2000204694E74046D1E7D0467BE7023B61449C +:10BB300032E7304609E76444023842E7704700BF05 +:10BB4000F8B500BFF8BC08BC9E467047F8B500BF0A +:10BB5000F8BC08BC9E467047088000200010020018 +:10BB60000338FDD87047000000000000000000000E +:10BB70000338FDD87047010000000000089900203C +:10BB80000338FDD87047416461444655004D6573E4 +:10BB90006874617374696300302E392E32207331FA +:10BBA000343020372E332E3000000000000000001B +:10BBB00000000000000000000023D1BCEA5F7823F1 +:10BBC00015DEEF12120000000000000070B000202F +:10BBD0004164616672756974006E52462055463242 +:10BBE00000303132333435363738394142434445F9 +:10BBF00046006E52462053657269616C0009045319 +:10BC00006F66744465766963653A200053002E00C0 +:10BC10006E6F7420666F756E640D0A00EB3C905574 +:10BC20004632205546322000020101000240000049 +:10BC300000F80201010001000000000009010100FC +:10BC4000800029420042004D455348544153544915 +:10BC5000430046415431362020203C21646F6374F8 +:10BC60007970652068746D6C3E0A3C68746D6C3E3A +:10BC70003C626F64793E3C7363726970743E0A6C17 +:10BC80006F636174696F6E2E7265706C6163652895 +:10BC90002268747470733A2F2F6275796D656163D1 +:10BCA0006F666665652E636F6D2F6D61726B2E62B8 +:10BCB0006972737322293B0A3C2F73637269707433 +:10BCC0003E3C2F626F64793E3C2F68746D6C3E0A77 +:10BCD00000000000494E464F5F554632545854000C +:10BCE00024850020494E44455820202048544D00CA +:10BCF0005ABC0F0043555252454E5420554632000F +:10BD00000000000021A70F0079A70F00A9A70F00CE +:10BD10004DA80F0005A90F00000000009DAB0F000B +:10BD2000ADAB0F00BDAB0F004DAC0F0069AD0F0008 +:10BD30000000000030313233343536373839616233 +:10BD4000636465666768696A6B6C6D6E6F7071724B +:10BD5000737475767778797A00000000000000002F +:10BD60002885FF7F010000000880002000000000FF +:10BD700000000000F48200205C830020C4830020C7 +:10BD800000000000000000000000000000000000B3 +:10BD900000000000000000000000000000000000A3 +:10BDA0000000000000000000000000000000000093 +:10BDB0000000000000000000000000000000000083 +:10BDC0000000000000000000000000000000000073 +:10BDD0000000000000000000000000000000000063 +:10BDE0000000000000000000000000000000000053 +:10BDF0000000000000000000000000000000000043 +:10BE00000000000000000000000000000000000032 +:10BE10000000000000000000010000000000000021 +:10BE20000E33CDAB34126DE6ECDE05000B000000E6 +:10BE30000000000000000000000000000000000002 +:10BE400000000000000000000000000000000000F2 +:10BE500000000000000000000000000000000000E2 +:10BE600000000000000000000000000000000000D2 +:10BE700000000000000000000000000000000000C2 +:10BE800000000000000000000000000000000000B2 +:10BE900000000000000000000000000000000000A2 +:10BEA0000000000000000000000000000000000092 +:10BEB0000000000000000000000000000000000082 +:10BEC0000000000000000000000000000000000072 +:10BED0000000000000000000000000000000000062 +:10BEE0000000000000000000000000000000000052 +:10BEF0000000000000000000000000000000000042 +:10BF00000000000000000000000000000000000031 +:10BF10000000000000000000000000000000000021 +:10BF20000000000000000000000000000000000011 +:10BF30000000000000000000000000000000000001 +:10BF400000000000000000000000000000000000F1 +:10BF500000000000000000000000000000000000E1 +:10BF600000000000000000000000000000000000D1 +:10BF700000000000000000000000000000000000C1 +:10BF800000000000000000000000000000000000B1 +:10BF900000000000000000000000000000000000A1 +:10BFA0000000000000000000000000000000000091 +:10BFB0000000000000000000000000000000000081 +:10BFC0000000000000000000000000000000000071 +:10BFD0000000000000000000000000000000000061 +:10BFE0000000000000000000000000000000000051 +:10BFF0000000000000000000000000000000000041 +:10C000000000000000000000000000000000000030 +:10C010000000000000000000000000000000000020 +:10C020000000000000000000000000000000000010 +:10C030000000000000000000000000000000000000 +:10C0400000000000000000000000000000000000F0 +:10C0500000000000000000000000000000000000E0 +:10C0600000000000000000000000000000000000D0 +:10C0700000000000000000000000000000000000C0 +:10C0800000000000000000000000000000000000B0 +:10C0900000000000000000000000000000000000A0 +:10C0A0000000000000000000000000000000000090 +:10C0B0000000000000000000000000000000000080 +:10C0C0000000000000000000000000000000000070 +:10C0D0000000000000000000000000000000000060 +:10C0E0000000000000000000000000000000000050 +:10C0F0000000000000000000000000000000000040 +:10C10000000000000000000000000000000000002F +:10C11000000000000000000000000000000000001F +:10C12000000000000000000000000000000000000F +:10C1300000000000000000000000000000000000FF +:10C1400000000000000000000000000000000000EF +:10C1500000000000000000000000000000000000DF +:10C1600000000000000000000000000000000000CF +:10C1700000000000000000000000000000000000BF +:10C1800000000000000000000000000000000000AF +:10C190000000000000000000FFFFFFFF7C7F002088 +:10C1A0000090D003FF00FFFF320000007D7B0F00F6 +:10C1B000F17B0F0001090262000301008032080BCD +:10C1C000000202020000090400000102020004054E +:10C1D0002400200105240100010424020205240694 +:10C1E00000010705810308001009040100020A008C +:10C1F000000007050202400000070582024000001F +:10C200000904020002080650050705030240000069 +:10C210000705830240000009024B00020100803242 +:10C22000080B0002020200000904000001020200E3 +:10C230000405240020010524010001042402020554 +:10C24000240600010705810308001009040100020B +:10C250000A000000070502024000000705820240B4 +:10C26000000012010002EF0201409A2329000001A0 +:10C2700001020301FDBB0F008DBB0F008DBB0F0042 +:10C2800064B30020F2BB0F00D9BB0F00554632202B +:10C29000426F6F746C6F6164657220302E392E327C +:10C2A000206C69622F6E726678202876322E302ECE +:10C2B0003029206C69622F74696E79757362202849 +:10C2C000302E31322E302D3134352D673937373518 +:10C2D000653736393129206C69622F75663220281E +:10C2E00072656D6F7465732F6F726967696E2F6306 +:10C2F0006F6E6669677570646174652D392D67614D +:10C30000646262386337290D0A4D6F64656C3A20A8 +:10C3100068747470733A2F2F6275796D6561636FFD +:10C32000666665652E636F6D2F6D61726B2E626937 +:10C330007273730D0A426F6172642D49443A204D45 +:10C340006573687461737469630D0A446174653A56 +:10C350002053657020203120323032340D0A000025 +:10C3600000000000000000000000000000000000CD +:10C3700000000000000000000000000000000000BD +:10C3800000000000000000000000000000000000AD +:10C39000000000000000000000000000000000009D +:10C3A000000000000000000000000000000000008D +:10C3B000000000000000000000000000000000007D +:10C3C000000000000000000000000000000000006D +:10C3D000000000000000000000000000000000005D +:10C3E000000000000000000000000000000000004D +:10C3F000000000000000000000000000000000003D +:10C40000000000000000000000000000010000002B +:10C4100098B4002010000C000000E0FF1F00000096 +:10C42000000000005D450F0069420F0041420F000F +:10D80000F1109E1E797A22200500000064000000BD +:10D81000CC00000000001000CD000000000004005B +:10D82000D000000029009A23D10000004028A5ADB7 +:10D83000D2000000200000000000000000000000F6 +:10D8400000000000000000000000000000000000D8 +:08D850000000000000000000D0 +:020000041000EA +:0810140000400F0000E00F0096 +:00000001FF diff --git a/bin/generic/Meshtastic_7.3.0_bootloader-0.9.2_s140_7.3.0.zip b/bin/generic/Meshtastic_7.3.0_bootloader-0.9.2_s140_7.3.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..bccd58625239b212c8db2bb00cab841b1b7c2129 GIT binary patch literal 192586 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⩔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%0D58=Ao1&aE=w9%(l}&h-bA!8ch@p?Q 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 literal 0 HcmV?d00001 diff --git a/bin/generic/update-Meshtastic_6.1.0_bootloader-0.9.2_nosd.uf2 b/bin/generic/update-Meshtastic_6.1.0_bootloader-0.9.2_nosd.uf2 new file mode 100644 index 0000000000000000000000000000000000000000..65032ce66e57cb31c7de84ba2dec18760aaaddba GIT binary patch literal 74752 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 a>mmHVvlK?Olgr=t_3x~kf1yB%|NjC8yB})+ literal 0 HcmV?d00001 diff --git a/bin/generic/update-Meshtastic_7.3.0_bootloader-0.9.2_nosd.uf2 b/bin/generic/update-Meshtastic_7.3.0_bootloader-0.9.2_nosd.uf2 new file mode 100644 index 0000000000000000000000000000000000000000..beffb3206f141e73c51046df8e0cce83d883bce9 GIT binary patch literal 74752 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

e*CXWQvIA-1uuNLnY*RYK-+ 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 Y#Q!@>VMIH*{C!{l&bs**3Z(e|FF42_X8-^I literal 0 HcmV?d00001 diff --git a/bin/genpartitions.py b/bin/genpartitions.py new file mode 100644 index 0000000..2e1a9f1 --- /dev/null +++ b/bin/genpartitions.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python2 + +# This is a layout for 4MB of flash +# Name, Type, SubType, Offset, Size, Flags +# nvs, data, nvs, 0x9000, 0x6000, +# otadata, data, ota, , 0x2000, +# app0, app, ota_0, , 0x1c0000, +# app1, app, ota_1, , 0x1c0000, +# spiffs, data, spiffs, , 0x06f000, + +start = 0x9000 +nvssys = 0x3000 +nvsuser = 0x2000 # NOTE: ti seems total size of nvssys MUST be 0x5000 or device will bootloop +nvs = nvssys + nvsuser +ota = 0x2000 +# app = 0x1c0000 +spi = 128 * 1024 + +# treat sys part sizes + spiffs size as reserved, then calculate what appsize can be +reserved = start + nvs + ota + spi +maxsize = 0x400000 # 4MB + +app = (maxsize - reserved) / 2 + +# total = start + nvs + ota + 2 * app + spi + +nvskb = nvsuser / 1024 +spikb = spi / 1024 +appkb = app / 1024 + +table = """ +# This is autogenerated by genpartions.py - change that tool instead! +# appsize={appkb} KB, spiffs={spikb} KB, usernvs={nvskb} KB +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x{start:x}, 0x{nvs:x}, +otadata, data, ota, , 0x{ota:x}, +app0, app, ota_0, , 0x{app:x}, +app1, app, ota_1, , 0x{app:x}, +spiffs, data, spiffs, , 0x{spi:x} """.format(**locals()) + +print(table) diff --git a/bin/lilygo_techo_bootloader-0.6.1.zip b/bin/lilygo_techo_bootloader-0.6.1.zip new file mode 100644 index 0000000000000000000000000000000000000000..34bd169f62a0f3913e2ebbc564455a2bef8098c4 GIT binary patch literal 190873 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

$&*Qh7qS<%!@nUL*MI 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

jqWDRdU%3zsH=u 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�hF6bG}d+&NiOQLS&25sIl1%U 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

d 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

guyN)-&D4*RZyzGw7}F7F7xLMB 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

;hP?-a_*c@MfW;siN=Mr7&&B&U!y||w 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 literal 0 HcmV?d00001 diff --git a/bin/meshtasticd.service b/bin/meshtasticd.service new file mode 100644 index 0000000..1e8ee98 --- /dev/null +++ b/bin/meshtasticd.service @@ -0,0 +1,16 @@ +[Unit] +Description=Meshtastic Native Daemon +After=network-online.target +StartLimitInterval=200 +StartLimitBurst=5 + +[Service] +User=root +Group=root +Type=simple +ExecStart=/usr/sbin/meshtasticd +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/bin/native-gdbserver.sh b/bin/native-gdbserver.sh new file mode 100644 index 0000000..f779d66 --- /dev/null +++ b/bin/native-gdbserver.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -e +pio run --environment native +gdbserver --once localhost:2345 .pio/build/native/program "$@" diff --git a/bin/native-install.sh b/bin/native-install.sh new file mode 100644 index 0000000..a8fcc29 --- /dev/null +++ b/bin/native-install.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +cp "release/meshtasticd_linux_$(uname -m)" /usr/sbin/meshtasticd +mkdir -p /etc/meshtasticd +if [[ -f "/etc/meshtasticd/config.yaml" ]]; then + cp bin/config-dist.yaml /etc/meshtasticd/config-upgrade.yaml +else + cp bin/config-dist.yaml /etc/meshtasticd/config.yaml +fi +cp bin/meshtasticd.service /usr/lib/systemd/system/meshtasticd.service diff --git a/bin/native-run.sh b/bin/native-run.sh new file mode 100644 index 0000000..6566fc5 --- /dev/null +++ b/bin/native-run.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -e +pio run --environment native +.pio/build/native/program "$@" diff --git a/bin/platformio-custom.py b/bin/platformio-custom.py new file mode 100644 index 0000000..0f099c0 --- /dev/null +++ b/bin/platformio-custom.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# trunk-ignore-all(ruff/F821) +# trunk-ignore-all(flake8/F821): For SConstruct imports +import sys +from os.path import join + +from readprops import readProps + +Import("env") +platform = env.PioPlatform() + + +def esp32_create_combined_bin(source, target, env): + # this sub is borrowed from ESPEasy build toolchain. It's licensed under GPL V3 + # https://github.com/letscontrolit/ESPEasy/blob/mega/tools/pio/post_esp32.py + print("Generating combined binary for serial flashing") + + app_offset = 0x10000 + + new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin") + sections = env.subst(env.get("FLASH_EXTRA_IMAGES")) + firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") + chip = env.get("BOARD_MCU") + flash_size = env.BoardConfig().get("upload.flash_size") + flash_freq = env.BoardConfig().get("build.f_flash", "40m") + flash_freq = flash_freq.replace("000000L", "m") + flash_mode = env.BoardConfig().get("build.flash_mode", "dio") + memory_type = env.BoardConfig().get("build.arduino.memory_type", "qio_qspi") + if flash_mode == "qio" or flash_mode == "qout": + flash_mode = "dio" + if memory_type == "opi_opi" or memory_type == "opi_qspi": + flash_mode = "dout" + cmd = [ + "--chip", + chip, + "merge_bin", + "-o", + new_file_name, + "--flash_mode", + flash_mode, + "--flash_freq", + flash_freq, + "--flash_size", + flash_size, + ] + + print(" Offset | File") + for section in sections: + sect_adr, sect_file = section.split(" ", 1) + print(f" - {sect_adr} | {sect_file}") + cmd += [sect_adr, sect_file] + + print(f" - {hex(app_offset)} | {firmware_name}") + cmd += [hex(app_offset), firmware_name] + + print("Using esptool.py arguments: %s" % " ".join(cmd)) + + esptool.main(cmd) + + +if platform.name == "espressif32": + sys.path.append(join(platform.get_package_dir("tool-esptoolpy"))) + import esptool + + env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) + + esp32_kind = env.GetProjectOption("custom_esp32_kind") + if esp32_kind == "esp32": + # Free up some IRAM by removing auxiliary SPI flash chip drivers. + # Wrapped stub symbols are defined in src/platform/esp32/iram-quirk.c. + env.Append( + LINKFLAGS=[ + "-Wl,--wrap=esp_flash_chip_gd", + "-Wl,--wrap=esp_flash_chip_issi", + "-Wl,--wrap=esp_flash_chip_winbond", + ] + ) + else: + # For newer ESP32 targets, using newlib nano works better. + env.Append(LINKFLAGS=["--specs=nano.specs", "-u", "_printf_float"]) + +if platform.name == "nordicnrf52": + env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex", + env.VerboseAction(f"{sys.executable} ./bin/uf2conv.py $BUILD_DIR/firmware.hex -c -f 0xADA52840 -o $BUILD_DIR/firmware.uf2", + "Generating UF2 file")) + +Import("projenv") + +prefsLoc = projenv["PROJECT_DIR"] + "/version.properties" +verObj = readProps(prefsLoc) +print("Using meshtastic platformio-custom.py, firmware version " + verObj["long"]) + +# General options that are passed to the C and C++ compilers +projenv.Append( + CCFLAGS=[ + "-DAPP_VERSION=" + verObj["long"], + "-DAPP_VERSION_SHORT=" + verObj["short"], + ] +) diff --git a/bin/promote-release.sh b/bin/promote-release.sh new file mode 100644 index 0000000..f96d2a5 --- /dev/null +++ b/bin/promote-release.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -e + +echo "This script is only for developers who are publishing new builds on github. Most users don't need it" + +VERSION=`bin/buildinfo.py long` + +# Must have a V prefix to trigger github +git tag "v${VERSION}" + +git push origin "v${VERSION}" # push the tag + +echo "Tag ${VERSION} pushed to github, github actions should now be building the draft release. If it seems good, click to publish it" diff --git a/bin/read-system-info.sh b/bin/read-system-info.sh new file mode 100644 index 0000000..b9e9c80 --- /dev/null +++ b/bin/read-system-info.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +esptool.py --baud 115200 read_flash 0x1000 0xf000 system-info.img diff --git a/bin/readprops.py b/bin/readprops.py new file mode 100644 index 0000000..4b73065 --- /dev/null +++ b/bin/readprops.py @@ -0,0 +1,42 @@ +import configparser +import subprocess + + +def readProps(prefsLoc): + """Read the version of our project as a string""" + + config = configparser.RawConfigParser() + config.read(prefsLoc) + version = dict(config.items("VERSION")) + verObj = dict( + short="{}.{}.{}".format(version["major"], version["minor"], version["build"]), + long="unset", + ) + + # Try to find current build SHA if if the workspace is clean. This could fail if git is not installed + try: + sha = ( + subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]) + .decode("utf-8") + .strip() + ) + isDirty = ( + subprocess.check_output(["git", "diff", "HEAD"]).decode("utf-8").strip() + ) + suffix = sha + # if isDirty: + # # short for 'dirty', we want to keep our verstrings source for protobuf reasons + # suffix = sha + "-d" + verObj["long"] = "{}.{}.{}.{}".format( + version["major"], version["minor"], version["build"], suffix + ) + except: + # print("Unexpected error:", sys.exc_info()[0]) + # traceback.print_exc() + verObj["long"] = verObj["short"] + + # print("firmware version " + verStr) + return verObj + + +# print("path is" + ','.join(sys.path)) diff --git a/bin/regen-protos.bat b/bin/regen-protos.bat new file mode 100644 index 0000000..7fa8f33 --- /dev/null +++ b/bin/regen-protos.bat @@ -0,0 +1 @@ +cd protobufs && ..\nanopb-0.4.9\generator-bin\protoc.exe --experimental_allow_proto3_optional "--nanopb_out=-S.cpp -v:..\src\mesh\generated" -I=..\protobufs\ ..\protobufs\meshtastic\*.proto diff --git a/bin/regen-protos.sh b/bin/regen-protos.sh new file mode 100644 index 0000000..12546bf --- /dev/null +++ b/bin/regen-protos.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e + +echo "This script requires https://jpa.kapsi.fi/nanopb/download/ version 0.4.9 to be located in the" +echo "firmware root directory if the following step fails, you should download the correct" +echo "prebuilt binaries for your computer into nanopb-0.4.9" + +# the nanopb tool seems to require that the .options file be in the current directory! +cd protobufs +../nanopb-0.4.9/generator-bin/protoc --experimental_allow_proto3_optional "--nanopb_out=-S.cpp -v:../src/mesh/generated/" -I=../protobufs meshtastic/*.proto diff --git a/bin/s140_nrf52_7.3.0_softdevice.hex b/bin/s140_nrf52_7.3.0_softdevice.hex new file mode 100644 index 0000000..639927f --- /dev/null +++ b/bin/s140_nrf52_7.3.0_softdevice.hex @@ -0,0 +1,9726 @@ +:020000040000FA +:1000000000040020810A000015070000610A0000BA +:100010001F07000029070000330700000000000050 +:10002000000000000000000000000000A50A000021 +:100030003D070000000000004707000051070000D6 +:100040005B070000650700006F07000079070000EC +:10005000830700008D07000097070000A10700003C +:10006000AB070000B5070000BF070000C90700008C +:10007000D3070000DD070000E7070000F1070000DC +:10008000FB070000050800000F0800001908000029 +:10009000230800002D080000370800004108000078 +:1000A0004B080000550800005F08000069080000C8 +:1000B000730800007D080000870800009108000018 +:1000C0009B080000A5080000AF080000B908000068 +:1000D000C3080000CD080000D7080000E1080000B8 +:1000E000EB080000F5080000FF0800000909000007 +:1000F000130900001D090000270900003109000054 +:100100003B0900001FB500F003F88DE80F001FBD8C +:1001100000F0ACBC40F6FC7108684FF01022401CA7 +:1001200008D00868401C09D00868401C04D0086842 +:1001300000F037BA9069F5E79069F9E7704770B554 +:100140000B46010B184400F6FF70040B4FF0805073 +:100150000022090303692403406943431D1B104621 +:1001600000F048FA29462046BDE8704000F042BA47 +:10017000F0B54FF6FF734FF4B4751A466E1E11E0DA +:10018000A94201D3344600E00C46091B30F8027B3B +:10019000641E3B441A44F9D19CB204EB134394B25D +:1001A00004EB12420029EBD198B200EB134002EBB2 +:1001B000124140EA0140F0BDF34992B00446D1E952 +:1001C0000001CDE91001FF224021684600F0F4FB58 +:1001D00094E80F008DE80F00684610A902E004C8FB +:1001E00041F8042D8842FAD110216846FFF7C0FF7C +:1001F0001090AA208DF8440000F099F9FFF78AFFCB +:1002000040F6FC7420684FF01025401C0FD0206889 +:1002100010226946803000F078F92068401C08D030 +:100220002068082210A900F070F900F061F9A869AF +:10023000EEE7A869F5E74FF080500369406940F6A2 +:10024000FC71434308684FF01022401C06D0086838 +:1002500000F58050834203D2092070479069F7E788 +:100260000868401C04D00868401C03D00020704778 +:100270009069F9E70420704770B504460068C34DE3 +:10028000072876D2DFE800F033041929631E250021 +:10029000D4E9026564682946304600F062F92A46CE +:1002A0002146304600F031F9AA002146304600F0E0 +:1002B00057FB002800D0032070BD00F009FC4FF46C +:1002C000805007E0201D00F040F90028F4D100F034 +:1002D000FFFB60682860002070BD241D94E80700C3 +:1002E000920000F03DFB0028F6D00E2070BDFFF715 +:1002F000A2FF0028FAD1D4E901034FF0805100EBAE +:10030000830208694D69684382420ED840F6F8704E +:1003100005684FF010226D1C09D0056805EB8305B8 +:100320000B6949694B439D4203D9092070BD55694A +:10033000F4E70168491C03D00068401C02D003E0C8 +:100340005069FAE70F2070BD2046FFF735FFFFF731 +:1003500072FF0028F7D1201D00F0F7F80028F2D135 +:1003600060680028F0D100F0E2F8FFF7D3FE00F05B +:10037000BFF8072070BD10B50C46182802D0012028 +:10038000086010BD2068FFF777FF206010BD41684E +:10039000054609B1012700E0002740F6F8742068FF +:1003A0004FF01026401C2BD02068AA68920000F065 +:1003B000D7FA38B3A86881002068401C27D020688D +:1003C000FFF7BDFED7B12068401C22D026684FF051 +:1003D0008050AC686D68016942695143A9420DD9EA +:1003E000016940694143A14208D92146304600F0E5 +:1003F000B8F822462946304600F087F800F078F831 +:100400007069D2E700F093F8FFF784FEF6E77069B1 +:10041000D6E77669DBE740F6FC7420684FF01026DB +:10042000401C23D02068401C0CD02068401C1FD0EA +:100430002568206805F18005401C1BD027683879A5 +:10044000AA2819D040F6F8700168491C42D001680A +:10045000491C45D00168491C3ED001680968491C07 +:100460003ED00168491C39D000683EE0B069DAE747 +:10047000B569DEE7B769E2E710212846FFF778FEA5 +:100480003968814222D12068401C05D0D4F8001080 +:1004900001F18002C03107E0B169F9E730B108CA63 +:1004A00051F8040D984201D1012000E000208A4259 +:1004B000F4D158B1286810B1042803D0FEE72846CB +:1004C000FFF765FF3149686808600EE0FFF722FE1C +:1004D00000F00EF87169BBE77169BFE7706904E06D +:1004E0004FF480500168491C01D000F0CBFAFEE7C0 +:1004F000BFF34F8F26480168264A01F4E06111439B +:100500000160BFF34F8F00BFFDE72DE9F0411746B3 +:100510000D460646002406E03046296800F054F8EF +:10052000641C2D1D361DBC42F6D3BDE8F08140F69B +:10053000FC700168491C04D0D0F800004FF48051D1 +:10054000FDE54FF010208069F8E74FF080510A690F +:10055000496900684A43824201D810207047002050 +:10056000704770B50C4605464FF4806608E0284693 +:1005700000F017F8B44205D3A4F5806405F5805562 +:10058000002CF4D170BD0000F40A0000000000202F +:100590000CED00E00400FA05144801680029FCD0C5 +:1005A0007047134A0221116010490B68002BFCD0E0 +:1005B0000F4B1B1D186008680028FCD0002010603D +:1005C00008680028FCD07047094B10B501221A605A +:1005D000064A1468002CFCD0016010680028FCD08A +:1005E0000020186010680028FCD010BD00E4014015 +:1005F00004E5014070B50C46054600F073F810B9EB +:1006000000F07EF828B121462846BDE8704000F091 +:1006100007B821462846BDE8704000F037B8000012 +:100620007FB5002200920192029203920A0B000B06 +:100630006946012302440AE0440900F01F0651F80C +:10064000245003FA06F6354341F82450401C8242F8 +:10065000F2D80D490868009A10430860081D016827 +:10066000019A1143016000F03DF800280AD00649C4 +:1006700010310868029A10430860091D0868039A3F +:10068000104308607FBD00000006004030B50F4CED +:10069000002200BF04EB0213D3F800582DB9D3F8A1 +:1006A000045815B9D3F808581DB1521C082AF1D3C3 +:1006B00030BD082AFCD204EB0212C2F80008C3F8CD +:1006C00004180220C3F8080830BD000000E0014013 +:1006D0004FF08050D0F83001082801D0002070473A +:1006E000012070474FF08050D0F83011062905D016 +:1006F000D0F83001401C01D0002070470120704725 +:100700004FF08050D0F830010A2801D00020704707 +:100710000120704708208F490968095808471020B0 +:100720008C4909680958084714208A4909680958FA +:100730000847182087490968095808473020854923 +:100740000968095808473820824909680958084744 +:100750003C20804909680958084740207D490968BC +:100760000958084744207B49096809580847482028 +:1007700078490968095808474C207649096809589A +:10078000084750207349096809580847542071499F +:1007900009680958084758206E49096809580847E8 +:1007A0005C206C4909680958084760206949096854 +:1007B00009580847642067490968095808476820AC +:1007C00064490968095808476C2062490968095852 +:1007D000084770205F4909680958084774205D4937 +:1007E00009680958084778205A490968095808478C +:1007F0007C205849096809580847802055490968EC +:10080000095808478420534909680958084788202F +:1008100050490968095808478C204E490968095809 +:10082000084790204B4909680958084794204949CE +:10083000096809580847982046490968095808472F +:100840009C204449096809580847A0204149096883 +:1008500009580847A4203F49096809580847A820B3 +:100860003C49096809580847AC203A4909680958C1 +:100870000847B0203749096809580847B420354966 +:10088000096809580847B8203249096809580847D3 +:10089000BC203049096809580847C0202D4909681B +:1008A00009580847C4202B49096809580847C82037 +:1008B0002849096809580847CC2026490968095879 +:1008C0000847D0202349096809580847D4202149FE +:1008D000096809580847D8201E4909680958084777 +:1008E000DC201C49096809580847E02019490968B3 +:1008F00009580847E4201749096809580847E820BB +:100900001449096809580847EC2012490968095830 +:100910000847F0200F49096809580847F4200D4995 +:10092000096809580847F8200A490968095808471A +:10093000FC2008490968095808475FF48070054998 +:10094000096809580847000003480449024A034B54 +:100950007047000000000020000B0000000B0000AA +:1009600040EA010310B59B070FD1042A0DD310C82C +:1009700008C9121F9C42F8D020BA19BA884201D97E +:10098000012010BD4FF0FF3010BD1AB1D30703D0C6 +:10099000521C07E0002010BD10F8013B11F8014B7C +:1009A0001B1B07D110F8013B11F8014B1B1B01D198 +:1009B000921EF1D1184610BD02F0FF0343EA032254 +:1009C00042EA024200F005B87047704770474FF0A6 +:1009D00000020429C0F0128010F0030C00F01B800C +:1009E000CCF1040CBCF1020F18BF00F8012BA8BF1A +:1009F00020F8022BA1EB0C0100F00DB85FEAC17CDE +:100A000024BF00F8012B00F8012B48BF00F8012B90 +:100A100070474FF0000200B51346944696462039C1 +:100A200022BFA0E80C50A0E80C50B1F12001BFF4A7 +:100A3000F7AF090728BFA0E80C5048BF0CC05DF80D +:100A400004EB890028BF40F8042B08BF704748BF5B +:100A500020F8022B11F0804F18BF00F8012B7047CF +:100A6000014B1B68DB6818470000002009480A4951 +:100A70007047FFF7FBFFFFF745FB00BD20BFFDE719 +:100A8000064B1847064A1060016881F308884068E1 +:100A900000470000000B0000000B000017040000DE +:100AA000000000201EF0040F0CBFEFF30881EFF3ED +:100AB0000981886902380078182803D100E0000015 +:100AC000074A1047074A12682C3212681047000084 +:100AD00000B5054B1B68054A9B58984700BD0000B0 +:100AE0007703000000000020F00A0000040000006E +:100AF000001000000000000000FFFFFF0090D00386 +:10100000C8130020395E020085C100009F5D020008 +:1010100085C1000085C1000085C1000000000000FE +:10102000000000000000000000000000C55E02009B +:1010300085C100000000000085C1000085C10000DE +:101040002D5F0200335F020085C1000085C10000F2 +:1010500085C1000085C1000085C1000085C1000078 +:10106000395F020085C1000085C100003F5F0200BA +:1010700085C10000455F02004B5F0200515F020026 +:1010800085C1000085C1000085C1000085C1000048 +:1010900085C1000085C1000085C1000085C1000038 +:1010A00085C10000575F020085C1000085C10000B6 +:1010B00085C1000085C1000085C1000085C1000018 +:1010C0005D5F020085C1000085C1000085C1000090 +:1010D00085C1000085C1000085C1000085C10000F8 +:1010E00085C1000085C1000085C1000085C10000E8 +:1010F00085C1000085C1000085C1000085C10000D8 +:1011000085C1000085C1000000F002F824F083FED4 +:101110000AA090E8000C82448344AAF10107DA4552 +:1011200001D124F078FEAFF2090EBAE80F0013F0F7 +:10113000010F18BFFB1A43F00103184718530200B0 +:10114000385302000A444FF0000C10F8013B13F032 +:10115000070408BF10F8014B1D1108BF10F8015B10 +:10116000641E05D010F8016B641E01F8016BF9D103 +:1011700013F0080F1EBF10F8014BAD1C0C1B09D15A +:101180006D1E58BF01F801CBFAD505E014F8016BCC +:1011900001F8016B6D1EF9D59142D6D3704700005E +:1011A0000023002400250026103A28BF78C1FBD870 +:1011B000520728BF30C148BF0B6070471FB500F011 +:1011C00003F88DE80F001FBD24F022BE70B51A4C45 +:1011D00005460A202070A01C00F0D5F85920A080F8 +:1011E00029462046BDE8704008F082B908F08BB966 +:1011F00070B50C461149097829B1A0F160015E294A +:1012000008D3012013E0602804D0692802D043F2FB +:1012100001000CE020CC0A4E94E80E0006EB8000A2 +:10122000A0F58050241FD0F8806E2846B04720607B +:1012300070BD012070470000080000201C00002045 +:10124000A05F02003249884201D20120704700208D +:10125000704770B50446A0F500002E4EB0F1786FCF +:1012600002D23444A4F500042948844201D2012565 +:1012700000E0002500F043F848B125B9B44204D39A +:101280002548006808E0012070BD002070BD002DD9 +:10129000F9D1B442F9D321488442F6D2F3E710B52C +:1012A0000446A0F50000B0F1786F03D21948044459 +:1012B000A4F5000400F023F84FF0804130B1164847 +:1012C000006804E08C4204D2012003E01348844209 +:1012D000F8D2002080F0010010BD10B520B1FFF75A +:1012E000DEFF08B1012010BD002010BD10B520B1F7 +:1012F000FFF7AFFF08B1012010BD002010BD084866 +:1013000008490068884201D10120704700207047D9 +:1013100000700200000000202000002008000020D3 +:101320005C000020BEBAFECA10B5044600210120B0 +:1013300000F042F800210B2000F03EF800210820C8 +:1013400000F03AF80421192000F036F804210D20AD +:1013500000F032F804210E2000F02EF804210F20B6 +:1013600000F02AF80421C84300F026F806211620D0 +:1013700000F022F80621152000F01EF82046FFF7A5 +:1013800025FF002010BD40F2231101807047FFF7B8 +:101390002DBF1148704710487047104A10B51468A7 +:1013A0000E4B0F4A08331A60FFF722FF0B48001D4F +:1013B000046010BD704770474907090E002804DB20 +:1013C00000F1E02080F80014704700F00F0000F1F9 +:1013D000E02080F8141D704703F900421005024018 +:1013E00001000001FD48002101604160018170475A +:1013F0002DE9FF4F93B09B46209F160004460DD069 +:101400001046FFF726FF18B1102017B0BDE8F08F87 +:101410003146012001F0D3FE0028F6D101258DF8D8 +:1014200042504FF4C050ADF84000002210A92846A9 +:1014300006F0C5FC0028E8D18DF84250A8464FF4CC +:1014400028500025ADF840001C2229466846079523 +:101450000DF01DF89DF81C000DF11C0A20F00F0086 +:10146000401C20F0F00010308DF81C0020788DF822 +:101470001D0061789DF81E000DF1400961F34200E6 +:1014800040F001008DF81E009DF8000008AA40F011 +:1014900002008DF800002089ADF83000ADF8325020 +:1014A0006089ADF83400CDF82CA060680E900AA9D0 +:1014B000CDF82890684606F090FA0028A5D160681B +:1014C000FFF70BFF40B16068FFF710FF20B96078AD +:1014D00000F00300022801D0012000E00020BF4CF2 +:1014E00008AA0AA92072BDF8200020808DF8428049 +:1014F00042F60120ADF840009DF81E0020F00600E5 +:10150000801C20F001008DF81E000220ADF8300094 +:10151000ADF8340014A80E90684606F05EFA002874 +:1015200089D1BDF82000608036B1211D304600F021 +:101530005FF90028C2D109E0BBF1000F05D00CF023 +:1015400021FDE8BB0CF01EFDD0BBA58017B1012F1B +:1015500043D04AE08DF8428042F6A620ADF8400024 +:1015600046461C220021684607950CF090FF9DF826 +:101570001C00ADF8346020F00F00401C20F0F0009B +:1015800010308DF81C009DF81D0020F0FF008DF834 +:101590001D009DF81E0020F0060040F00100801C98 +:1015A0008DF81E009DF800008DF8446040F00200A8 +:1015B0008DF80000CDE90A9AADF8306011A800E07E +:1015C00011E00E9008AA0AA9684606F006FA00285B +:1015D000A6D1BDF82000E08008E00CF0D3FC10B9E3 +:1015E0000CF0D0FC08B103200FE7E58000200CE7E9 +:1015F0003EB50446794D0820ADF80000A88828B112 +:101600002046FFF726FE18B110203EBD06203EBD45 +:101610002146012001F0D3FD0028F8D12088ADF843 +:1016200004006088ADF80600A088ADF80800E088E6 +:10163000ADF80A00A88801AB6A46002106F0AAFDB1 +:10164000BDF800100829E2D003203EBD7FB5634DF0 +:101650000446A88868B1002002900820ADF8080070 +:10166000CDF80CD02046FFF7F4FD20B1102004B0D7 +:1016700070BD0620FBE7A98802AA4FF6FF7006F0AE +:10168000CCFF0028F3D1BDF80810082901D00320B1 +:10169000EDE7BDF800102180BDF802106180BDF8B3 +:1016A0000410A180BDF80610E180E0E701B582B02A +:1016B0000220ADF80000494802AB6A46408800218C +:1016C00006F068FDBDF80010022900D003200EBD11 +:1016D0001CB5002100910221ADF800100190FFF728 +:1016E000DEFD08B110201CBD3C486A4641884FF61B +:1016F000FF7006F092FFBDF800100229F3D003201E +:101700001CBDFEB5354C06461546207A0F46C0076F +:1017100005D00846FFF79DFD18B11020FEBD0F2033 +:10172000FEBDF82D01D90C20FEBD3046FFF791FD1E +:1017300018BB208801A905F03AFE0028F4D13078C2 +:101740008DF80500208801A906F003FD0028EBD1E3 +:1017500000909DF800009DF8051040F002008DF803 +:101760000000090703D040F008008DF80000208831 +:10177000694606F08BFC0028D6D1ADF808502088C9 +:101780003B4602AA002106F005FDBDF80810A9425B +:10179000CAD00320FEBD7CB5054600200090019014 +:1017A0000888ADF800000C4628460195FFF795FD26 +:1017B00018B92046FFF773FD08B110207CBD15B1A4 +:1017C000BDF8000060B105486A4601884FF6FF7019 +:1017D00006F023FFBDF8001021807CBD240200200C +:1017E0000C20FAE72F48C088002800D0012070475D +:1017F00030B5044693B000200D46014600901422F7 +:1018000001A80CF044FE1C22002108A80CF03FFEA9 +:101810009DF80000CDF808D020F00F00401C20F00B +:10182000F00010308DF800009DF8010006AA20F0AD +:10183000FF008DF801009DF8200001A940F0020092 +:101840008DF8200001208DF8460042F60420ADF806 +:10185000440011A801902088ADF83C006088ADF8E4 +:101860003E00A088ADF84000E088ADF842009DF849 +:10187000020020F00600801C20F001008DF802001C +:101880000820ADF80C00ADF810000FA8059008A8CE +:1018900006F0A3F8002803D1BDF818002880002026 +:1018A00013B030BD24020020F0B5007B059F1E461A +:1018B00014460D46012800D0FFDF0C2030803A206E +:1018C0003880002C08D0287A032806D0287B0128ED +:1018D00000D0FFDF17206081F0BDA889FBE72DE96C +:1018E000F0470D4686B095F80C900E991446B9F164 +:1018F000010F0BD01022007B2E8A9046052807D0BE +:10190000062839D0FFDF06B0BDE8F0870222F2E7F3 +:10191000E8890C2200EB400002EB400018803320E5 +:101920000880002CEFD0E8896081002720E0009635 +:10193000688808F1020301AA696900F097FF06EBC5 +:101940000800801C07EB470186B204EB4102BDF89A +:1019500004009081F848007808B1012300E00023DA +:101960000DF1060140460E3214F029F87F1CBFB27B +:101970006089B842DBD8C6E734200880E889B9F12D +:10198000010F11D0122148430E301880002CBAD01C +:10199000E88960814846B9F1010F00D00220207328 +:1019A00000270DF1040A1FE00621ECE70096688885 +:1019B00008F1020301AA696900F058FF06EB08006C +:1019C000801C86B2B9F1010F12D007EBC70004EBFF +:1019D0004000BDF80410C18110220AF1020110304C +:1019E0000CF02BFD7F1CBFB26089B842DED88AE7BD +:1019F00007EB470104EB4102BDF80400D0810AF176 +:101A000002014046103213F0FCFFEBE72DE9F047EE +:101A10000E4688B090F80CC096F80C80378AF5898D +:101A20000C20DFF81493109902F10C04BCF1030FA1 +:101A300008D0BCF1040F3DD0BCF1070F75D0FFDF1B +:101A400008B061E705EB850C00EB4C0018803120F5 +:101A50000880002AF4D0A8F1060000F0FF0A5581A2 +:101A600024E01622002101A80CF011FD00977088D7 +:101A7000434601AA716900F0F9FEBDF80400208018 +:101A8000BDF80600E080BDF80800208199F800004C +:101A900008B1012300E00023A21C0DF10A01504609 +:101AA00013F08DFF07EB080087B20A346D1EADB24C +:101AB000D7D2C5E705EB850C00EB4C00188032202F +:101AC0000880002ABCD0A8F1050000F0FF0A55816B +:101AD00037E000977088434601AA716900F0C6FE9E +:101AE0009DF80600BDF80410E1802179420860F3FA +:101AF000000162F34101820862F38201C20862F3CD +:101B0000C301020962F30411420962F3451182091B +:101B100062F386112171C0096071BDF80700208150 +:101B200099F8000010B1012301E00EE000232246E5 +:101B30000DF10901504613F042FF07EB080087B290 +:101B40000A346D1EADB2C4D27AE7A8F1020084B2A5 +:101B500005FB08FC0CF10E00188035200880002AD7 +:101B6000A7D05581948100971FFA8CF370880E32AC +:101B7000716900F07BFE63E72DE9F84F1E460A9D70 +:101B80000C4681462AB1607A00F58070D080E089E9 +:101B9000108199F80C000C274FF000084FF00E0A46 +:101BA0000D2872D2DFE800F09D070E1B272F374566 +:101BB000546972727200214648460095FFF774FE20 +:101BC000BDE8F88F207B9146082802D0032800D07A +:101BD000FFDF3780302009E0A9F80A80F0E7207B9A +:101BE0009146042800D0FFDF378031202880B9F1EA +:101BF000000FF1D1E4E7207B9146042800D0FFDFFD +:101C000037803220F2E7207B9146022800D0FFDFA8 +:101C100037803320EAE7207B1746022800D0FFDF19 +:101C20003420A6F800A02880002FC9D0A7F80A8089 +:101C3000C6E7207B1746042800D0FFDF3520A6F832 +:101C400000A02880002FBBD04046A7F80A8012E0F1 +:101C5000207B1746052802D0062800D0FFDF102081 +:101C6000308036202880002FAAD0E0897881A7F81C +:101C70000E80B9F80E00B881A2E7207B91460728B4 +:101C800000D0FFDF37803720B0E72AE04FF01200A6 +:101C900018804FF038001700288091D0E0897881B3 +:101CA000A7F80E80A7F8108099F80C000A2805D034 +:101CB0000B2809D00C280DD0FFDF81E7207B0A28F4 +:101CC00000D0FFDF01200AE0207B0B2800D0FFDFDF +:101CD000042004E0207B0C2800D0FFDF05203873AF +:101CE0006EE7FFDF6CE770B50C46054601F0AAFB16 +:101CF00020B10078222804D2082070BD43F20200EF +:101D000070BD0521284612F0D1F8206008B10020EE +:101D100070BD032070BD30B44880087820F00F00FB +:101D2000C01C20F0F000903001F8080B1DCA81E8BB +:101D30001D0030BC07F05DBC100000202DE9FF47FE +:101D400084B0002782460297079890468946123051 +:101D50000AF069FA401D20F00306079828B907A980 +:101D60005046FFF7C0FF002854D1B9F1000F05D04D +:101D70000798017B19BB052504681BE098F8000053 +:101D8000092803D00D2812D0FFDF46E0079903256C +:101D90004868B0B3497B42887143914239D98AB2CD +:101DA000B3B2011D11F0F5FE0446078002E0079C66 +:101DB000042508340CB1208810B1032D29D02CE063 +:101DC0000798012112300AF060FAADF80C000246C3 +:101DD00002AB2946504608F0B8FA070001D1A01C12 +:101DE000029007983A461230C8F80400A8F802A0FA +:101DF00003A94046029B0AF055FAD8B10A2817D227 +:101E000000E006E0DFE800F007091414100B0D14E1 +:101E10001412132014E6002012E6112010E6082008 +:101E20000EE643F203000BE6072009E60D2007E665 +:101E3000032005E6BDF80C002346CDE900702A46D4 +:101E40005046079900F022FD57B9032D08D1079895 +:101E5000B3B2417B406871438AB2011D11F0ADFEFF +:101E6000B9F1000FD7D0079981F80C90D3E72DE98D +:101E7000FE4F91461A881C468A468046FAB102AB4C +:101E8000494608F062FA050019D04046A61C27888A +:101E900012F04FF93246072629463B46009611F0CC +:101EA0005EFD20882346CDE900504A465146404613 +:101EB00000F0ECFC002020800120BDE8FE8F002017 +:101EC000FBE710B586B01C46AAB104238DF800309C +:101ED0001388ADF808305288ADF80A208A788DF85A +:101EE0000E200988ADF80C1000236A462146FFF742 +:101EF00025FF06B010BD1020FBE770B50D4605218B +:101F000011F0D4FF040000D1FFDF294604F11200D4 +:101F1000BDE870400AF0A2B92DE9F8430D468046AD +:101F2000002607F063FB04462878102878D2DFE803 +:101F300000F0773B345331311231313108313131D6 +:101F400031312879001FC0B2022801D0102810D1E9 +:101F500014BBFFDF35E004B9FFDF0521404611F077 +:101F6000A5FF007B032806D004280BD0072828D023 +:101F7000FFDF072655E02879801FC0B2022820D055 +:101F800050B1F6E72879401FC0B2022819D01028B6 +:101F900017D0EEE704B9FFDF13E004B9FFDF2879BB +:101FA00001280ED1172137E00521404611F07EFFB0 +:101FB000070000D1FFDF07F1120140460AF02BF9BC +:101FC0002CB12A4621464046FFF7A5FE29E0132101 +:101FD000404602F01FFD24E004B9FFDF0521404622 +:101FE00011F064FF060000D1FFDF694606F1120020 +:101FF0000AF01BF9060000D0FFDFA988172901D2DB +:10200000172200E00A46BDF80000824202D90146CC +:1020100002E005E01729C5D3404600F047FCD0E7B1 +:10202000FFDF3046BDE8F883401D20F0030219B100 +:1020300002FB01F0001D00E000201044704713B5C2 +:10204000009858B10024684611F04DFD002C04D1D1 +:10205000F749009A4A6000220A701CBD0124002042 +:10206000F2E72DE9F0470C461546242200212046D0 +:102070000CF00DFA05B9FFDFA87860732888DFF847 +:10208000B0A3401D20F00301AF788946DAF80400C0 +:1020900011F047FD060000D1FFDF4FF00008266079 +:1020A000A6F8008077B109FB07F1091D0AD0DAF81C +:1020B000040011F036FD060000D1FFDF6660C6F8AF +:1020C000008001E0C4F80480298804F11200BDE812 +:1020D000F0470AF091B82DE9F047804601F112006F +:1020E0000D4681460AF09FF8401DD14F20F00302B3 +:1020F0006E7B14462968786811F03EFD3EB104FB02 +:1021000006F2121D03D06968786811F035FD0520CC +:1021100011F074FE0446052011F078FE201A012803 +:1021200002D1786811F0F2FC49464046BDE8F0471C +:102130000AF078B870B50546052111F0B7FE040025 +:1021400000D1FFDF04F112012846BDE870400AF01B +:1021500062B82DE9F04F91B04FF0000BADF828B008 +:10216000ADF804B047880C4605469246052138462E +:1021700011F09CFE060000D1FFDF24B1A780A4F877 +:1021800006B0A4F808B0297809220B20B2EB111F81 +:1021900073D12A7A04F1100138274FF00C084FF060 +:1021A00012090291102A69D2DFE802F068F2F1F018 +:1021B0008008D3898EA03DDCF3EEB7B7307B0228D0 +:1021C00000D0FFDFA88908EBC001ADF80410302172 +:1021D000ADF82810002C25D06081B5F80E800027BE +:1021E0001DE004EBC709317C89F80E10F189A9F8CC +:1021F0000C10CDF800806888042305AA296900F036 +:1022000035FBBDF81410A9F8101008F10400BDF852 +:1022100016107F1C1FFA80F8A9F81210BFB260894F +:10222000B842DED80CE1307B022800D0FFDFE9891C +:1022300008EBC100ADF804003020ADF8280095F897 +:102240000C90002CA9F10400C0B20F90EAD061817B +:10225000B5F81080002725E0CDF8008068884B464F +:1022600003AA696900F002FB08EB09001FFA80F875 +:102270006F48007818B1012302E0DDE0DAE00023C6 +:1022800004EBC702009204A90C320F9813F097FBDD +:10229000009ABDF80C007F1C1082009ABDF80E0059 +:1022A000BFB250826089B842D6D8C9E00AA800906F +:1022B00001AB224629463046FFF711FBC0E0307BD8 +:1022C000082805D0FFDF03E0307B082800D0FFDFBF +:1022D000E8891030ADF804003620ADF82800002C55 +:1022E0003FD0A9896181F189A18127E0307B09284C +:1022F00000D0FFDFA88901460C30ADF8040037207C +:10230000ADF82800002C2CD06181E8890090AB89C1 +:10231000688804F10C02296955E0E88939211030F8 +:1023200080B2ADF80400ADF82810002C72D0A98955 +:102330006181287A0E280AD002212173E989E1817E +:10234000288A0090EB8968886969029A3BE001213C +:10235000F3E70AA8009001AB224629463046FFF772 +:1023600055FB6DE0307B0A2800D0FFDFADF804900C +:10237000ADF828704CB3A9896181A4F810B0A4F815 +:102380000EB0012020735BE020E002E030E038E096 +:1023900041E0307B0B2800D0FFDF288AADF82870A1 +:1023A0001230ADF8040084B104212173A989618140 +:1023B000E989E181298A2182688A00902B8A6888CC +:1023C00004F11202696900F051FA39E0307B0C28FF +:1023D00000D0FFDFADF80490ADF828703CB30521C4 +:1023E0002173A4F80AB0A4F80EB0A4F810B027E046 +:1023F0000AA8009001AB224629463046FFF754FA5E +:102400001EE00AA8009001AB224629463046FFF79D +:10241000B3FB15E034E03B21ADF80400ADF8281023 +:1024200074B30120E080A4F808B084F80AB007E093 +:1024300010000020FFDF03E0297A012917D0FFDF19 +:10244000BDF80400AAF800006CB1BDF82800208097 +:10245000BDF804006080BDF82800392803D03C286E +:1024600001D086F80CB011B00020BDE8F08F3C21FF +:10247000ADF80400ADF8281014B1697AA172DFE755 +:10248000AAF80000EFE72DE9F84356880F4680468A +:1024900015460521304611F009FD040000D1FFDF8B +:1024A000123400943B46414630466A680AF02EF8E2 +:1024B000B8E570B50D46052111F0F8FC040000D117 +:1024C000FFDF294604F11200BDE8704009F0B8BEF4 +:1024D00070B50D46052111F0E9FC040000D1FFDFC5 +:1024E000294604F11200BDE8704009F0D6BE70B56F +:1024F0000546052111F0DAFC040000D1FFDF04F1EC +:10250000080321462846BDE870400422AFE470B5B8 +:102510000546052111F0CAFC040000D1FFDF214669 +:1025200028462368BDE870400522A0E470B5064641 +:10253000052111F0BBFC040000D1FFDF04F1120003 +:1025400009F071FE401D20F0030511E0011D008817 +:102550000322431821463046FFF789FC00280BD0A0 +:10256000607BABB2684382B26068011D11F05BFB17 +:10257000606841880029E9D170BD70B50E460546F6 +:1025800007F034F8040000D1FFDF012020726672EA +:102590006580207820F00F00C01C20F0F000303063 +:1025A0002070BDE8704007F024B8602801D00720F3 +:1025B00070470878C54900F0010008700020704796 +:1025C0002DE9F0438BB00D461446814606A9FFF76E +:1025D0008AFB002814D14FF6FF7601274FF42058CC +:1025E0008CB103208DF800001020ADF8100007A872 +:1025F000059007AA204604A913F005FA78B1072030 +:102600000BB0BDE8F0830820ADF808508DF80E70CF +:102610008DF80000ADF80A60ADF80C800CE006986B +:10262000A17801742188C1818DF80E70ADF8085031 +:10263000ADF80C80ADF80A606A4602214846069B58 +:10264000FFF77CFBDCE708B501228DF8022042F69B +:102650000202ADF800200A4603236946FFF731FC69 +:1026600008BD08B501228DF8022042F60302ADF83C +:1026700000200A4604236946FFF723FC08BD00B585 +:1026800087B079B102228DF800200A88ADF80820C1 +:102690004988ADF80A1000236A460521FFF74EFB72 +:1026A00007B000BD1020FBE709B1072309E40720AC +:1026B000704770B588B00D461446064606A9FFF768 +:1026C00012FB00280ED17CB10620ADF808508DF821 +:1026D0000000ADF80A40069B6A460821DC813046BE +:1026E000FFF72CFB08B070BD05208DF80000ADF899 +:1026F0000850F0E700B587B059B107238DF80030D6 +:10270000ADF80820039100236A460921FFF716FB64 +:10271000C6E71020C4E770B588B00C460646002511 +:1027200006A9FFF7E0FA0028DCD106980121123053 +:1027300009F0ABFD9CB12178062921D2DFE801F038 +:10274000200505160318801E80B2C01EE28880B2E4 +:102750000AB1A3681BB1824203D90C20C2E7102042 +:10276000C0E7042904D0A08850B901E00620B9E7E9 +:10277000012913D0022905D004291CD005292AD00B +:102780000720AFE709208DF800006088ADF8080049 +:10279000E088ADF80A00A068039023E00A208DF8D5 +:1027A00000006088ADF80800E088ADF80A00A06875 +:1027B0000A25039016E00B208DF800006088ADF824 +:1027C0000800A088ADF80A00E088ADF80C00A06809 +:1027D0000B25049006E00C208DF8000060788DF841 +:1027E00008000C256A4629463046069BFFF7A6FAE4 +:1027F00078E700B587B00D228DF80020ADF80810FD +:1028000000236A461946FFF799FA49E700B587B0F1 +:1028100071B102228DF800200A88ADF8082049889D +:10282000ADF80A1000236A460621FFF787FA37E75A +:10283000102035E770B586B0064601200D46ADF88C +:1028400008108DF80000014600236A463046FFF765 +:1028500075FA040008D12946304605F0B5FC002180 +:10286000304605F0CFFC204606B070BDF8B51C46DA +:1028700015460E46069F11F04AFC2346FF1DBCB2CA +:1028800031462A46009411F036F8F8BD30B41146AE +:10289000DDE902423CB1032903D0002330BC08F03B +:1028A00032BE0123FAE71A8030BC704770B50C467F +:1028B0000546FFF722FB2146284605F094FC2846F2 +:1028C000BDE87040012105F09DBC00001000002013 +:1028D0004FF0E0224FF400400021C2F88001BFF326 +:1028E0004F8FBFF36F8F1748016001601649900248 +:1028F00008607047134900B500220A600A60124B55 +:102900004FF060721A60002808BF00BD0F4A104BDC +:10291000DFF840C001280CD002281CBFFFDF00BD3B +:10292000032008601A604FF4000000BFCCF80000DC +:1029300000BD022008601A604FF04070F6E700B555 +:10294000FFDF00BD00F5004008F50140A4020020B3 +:1029500014F5004004F5014070B50B2000F0BDF9FE +:10296000082000F0BAF900210B2000F0D4F9002172 +:10297000082000F0D0F9F44C01256560A560002026 +:10298000C4F84001C4F84401C4F848010B2000F029 +:10299000B5F9082000F0B2F90B2000F091F925609C +:1029A00070BD10B50B2000F098F9082000F095F9E3 +:1029B000E548012141608160E4490A68002AFCD1B0 +:1029C0000021C0F84011C0F84411C0F848110B2094 +:1029D00000F094F9BDE81040082000F08FB910B560 +:1029E0000B2000F08BF9BDE81040082000F086B9FC +:1029F00000B530B1012806D0022806D0FFDF002044 +:102A000000BDD34800BDD34800BDD248001D00BD65 +:102A100070B5D1494FF000400860D04DC00BC5F8EB +:102A20000803CF4800240460C5F840410820C4359D +:102A300000F053F9C5F83C41CA48047070BD08B5B0 +:102A4000C14A002128B1012811D002281CD0FFDF83 +:102A500008BD4FF48030C2F80803C2F84803BB48F1 +:102A60003C300160C2F84011BDE80840D0E74FF4A7 +:102A70000030C2F80803C2F84803B448403001608F +:102A8000C2F84411B3480CE04FF48020C2F80803A8 +:102A9000C2F84803AD4844300160C2F84811AD485F +:102AA000001D0068009008BD70B516460D4604462E +:102AB000022800D9FFDF0022A348012304F11001FE +:102AC0008B4000EB8401C1F8405526B1C1F840218C +:102AD000C0F8043303E0C0F80833C1F84021C0F85F +:102AE000443370BD2DE9F0411D46144630B1012834 +:102AF00033D0022838D0FFDFBDE8F081891E0022E4 +:102B000021F07F411046FFF7CFFF012D23D0002099 +:102B1000944D924F012668703E61914900203C39E6 +:102B200008600220091D08608D49042030390860C2 +:102B30008B483D34046008206C6000F0DFF83004FE +:102B4000C7F80403082000F0BBF88349F007091F09 +:102B500008602E70D0E70120DAE7012B02D00022B6 +:102B6000012005E00122FBE7012B04D00022022016 +:102B7000BDE8F04198E70122F9E774480068704722 +:102B800070B500F0D8F8704C0546D4F84001002626 +:102B9000012809D1D4F80803C00305D54FF48030CB +:102BA000C4F80803C4F84061D4F8440101280CD1EA +:102BB000D4F80803800308D54FF40030C4F80803A4 +:102BC000C4F84461012013F0EEFED4F84801012856 +:102BD0000CD1D4F80803400308D54FF48020C4F882 +:102BE0000803C4F84861022013F0DDFE5E4805606A +:102BF00070BD70B500F09FF85A4D0446287850B16A +:102C0000FFF706FF687818B10020687013F0CBFE5C +:102C10005548046070BD0320F8E74FF0E0214FF401 +:102C20000010C1F800027047152000F067B84B494A +:102C300001200861082000F061B848494FF47C1079 +:102C4000C1F808030020024601EB8003C3F84025C9 +:102C5000C3F84021401CC0B20628F5D37047410A92 +:102C600043F609525143C0F3080010FB02F000F58F +:102C7000807001EB5020704710B5430B48F2376469 +:102C800063431B0C5C020C60384C03FB0400384BA4 +:102C90004CF2F72443435B0D13FB04F404EB402098 +:102CA00000F580704012107008681844086010BD6C +:102CB0002C484068704729490120C1F8000270473C +:102CC000002809DB00F01F0201219140400980002B +:102CD00000F1E020C0F80011704700280DDB00F083 +:102CE0001F02012191404009800000F1E020C0F85E +:102CF0008011BFF34F8FBFF36F8F7047002809DB40 +:102D000000F01F02012191404009800000F1E02005 +:102D1000C0F8801270474907090E002804DB00F153 +:102D2000E02080F80014704700F00F0000F1E02070 +:102D300080F8141D70470C48001F00680A4A0D49AE +:102D4000121D11607047000000B0004004B5004043 +:102D50004081004044B1004008F50140008000403F +:102D6000408500403C00002014050240F7C2FFFFF0 +:102D70006F0C0100010000010A4810B50468094900 +:102D800009480831086013F0A2FE0648001D0460DF +:102D900010BD0649002008604FF0E0210220C1F874 +:102DA000800270471005024001000001FC1F004036 +:102DB000374901200860704770B50D2000F049F8D0 +:102DC000344C0020C4F800010125C4F804530D2040 +:102DD00000F050F825604FF0E0216014C1F80001C8 +:102DE00070BD10B50D2000F034F82A480121416073 +:102DF0000021C0F80011BDE810400D2000F03AB8E5 +:102E0000254810B504682449244808310860214940 +:102E1000D1F80001012804D0FFDF1F48001D046025 +:102E200010BD1B48001D00680022C0B2C1F800217F +:102E300014F07FFBF1E710B5164800BFD0F8001181 +:102E40000029FBD0FFF7DCFFBDE810400D2000F0AB +:102E500011B800280DDB00F01F020121914040094C +:102E6000800000F1E020C0F88011BFF34F8FBFF366 +:102E70006F8F7047002809DB00F01F02012191408D +:102E80004009800000F1E020C0F880127047000087 +:102E900004D5004000D000401005024001000001B0 +:102EA0004FF0E0214FF00070C1F8800101F5C071D2 +:102EB000BFF34F8FBFF36F8FC1F80001394B8022F2 +:102EC00083F8002441F8800C704700B502460420C6 +:102ED000354903E001EBC0031B792BB1401EC0B2A2 +:102EE000F8D2FFDFFF2000BD41F8302001EBC00128 +:102EF00000224A718A7101220A7100BD2A4A00210A +:102F000002EBC0000171704710B50446042800D3DD +:102F1000FFDF254800EBC4042079012800D0FFDF43 +:102F20006079A179401CC0B2814200D060714FF03D +:102F3000E0214FF00070C1F8000210BD70B504250B +:102F4000194E1A4C16E0217806EBC1000279012ACD +:102F500008D1427983799A4204D04279827156F835 +:102F6000310080472078401CC0B22070042801D373 +:102F7000002020706D1EEDB2E5D270BD0C4810B57A +:102F800004680B490B4808310860064890F80004B3 +:102F90004009042800D0FFDFFFF7D0FF0448001DE0 +:102FA000046010BD19E000E0E0050020580000209A +:102FB00010050240010000010548064A01689142DF +:102FC00001D1002101600449012008607047000020 +:102FD0005C000020BEBAFECA40E5014070B50C4658 +:102FE000054609F02FFC21462846BDE870400AF04E +:102FF00010BD7047704770470021016081807047A5 +:103000002CFFFFFFDBE5B151007002002301FFFF41 +:103010008C00000078DB6A007A2E9AC67DB66CFAC6 +:10302000F35721CCC310D5E51471FB3C30B5FC4DF2 +:103030000446062CA9780ED2DFE804F0030E0E0E2B +:103040000509FFDF08E0022906D0FFDF04E00329BD +:1030500002D0FFDF00E0FFDFAC7030BD30B50446CA +:103060001038EF4D07280CD2DFE800F0040C060CF6 +:103070000C0C0C00FFDF05E0287E112802D0FFDFDA +:1030800000E0FFDF2C7630BD2DE9F04112F026FE86 +:10309000044614F063F8201AC5B2062010F0AEFE04 +:1030A0000446062010F0B2FE211ADD4C207E1228C4 +:1030B00018D000200F18072010F0A0FE06460720A9 +:1030C00010F0A4FE301A3918207E13280CD00020EE +:1030D0000144A078042809D000200844281AC0B26E +:1030E000BDE8F0810120E5E70120F1E70120F4E7E8 +:1030F000CB4810B590F825004108C94800F12600DA +:1031000005D00EF0F5FEBDE8104006F08CB80EF0CC +:10311000D0FEF8E730B50446A1F120000D460A289C +:103120004AD2DFE800F005070C1C2328353A3F445B +:10313000FFDF42E0207820283FD1FFDF3DE0B848A4 +:103140008178052939D0007E122836D020782428AD +:1031500033D0252831D023282FD0FFDF2DE0207851 +:1031600022282AD0232828D8FFDF26E0207822280A +:1031700023D0FFDF21E0207822281ED024281CD075 +:1031800026281AD0272818D0292816D0FFDF14E0C7 +:103190002078252811D0FFDF0FE0207825280CD0DB +:1031A000FFDF0AE02078252807D0FFDF05E0207840 +:1031B000282802D0FFDF00E0FFDF257030BD1FB5FB +:1031C00004466A46002001F0A5FEB4B1BDF8022015 +:1031D0004FF6FF700621824201D1ADF80210BDF812 +:1031E0000420824201D1ADF80410BDF808108142DC +:1031F00003D14FF44860ADF8080068460FF0E2FADA +:1032000006F011F804B010BD70B516460C46054620 +:10321000FEF71FF848B90CB1B44208D90C2070BDB4 +:1032200055F82400FEF715F808B1102070BD2046AF +:10323000641EE4B2F4D270BD2DE9F04105461F468C +:1032400090460E4600240068FEF750F830B9A98871 +:1032500028680844401EFEF749F808B110203FE7EF +:1032600028680028A88802D0B84202D850E0002878 +:10327000F5D0092034E72968085DB8B1671CCA5D3C +:10328000152A2ED03CDC152A3AD2DFE802F039129A +:10329000222228282A2A313139393939393939391C +:1032A00039392200085D30BB641CA4B2A242F9D8AF +:1032B00033E00228DDD1A01C085C88F80000072854 +:1032C00001D2400701D40A200AE7307840F001001B +:1032D00015E0C143C90707E0012807D010E0062028 +:1032E000FEE60107A1F180510029F5D01846F7E666 +:1032F0003078810701D50B20F2E640F002003070F3 +:103300002868005D384484B2A888A04202D2B0E7A1 +:103310004FF4485382B2A242ADD80020E0E610B587 +:10332000027843F2022354080122022C12D003DC5B +:103330003CB1012C16D106E0032C10D07F2C11D10A +:1033400012E0002011E080790324B4EB901F09D132 +:103350000A700BE08079B2EB901F03D1F8E7807917 +:103360008009F5D0184610BDFF200870002010BD60 +:1033700008B500208DF80000294890F82E1051B1B2 +:1033800090F82F0002280FD003280FD0FFDF00BFD6 +:103390009DF8000008BD22486946253001F009FE6D +:1033A0000028F5D0FFDFF3E7032000E001208DF8CF +:1033B0000000EDE738B50C460546694601F0F9FD19 +:1033C00000280DD19DF80010207861F3470020708F +:1033D00055F8010FC4F80100A888A4F805000020E2 +:1033E00038BD38B5137888B102280FD0FF281BD01C +:1033F0000CA46D46246800944C7905EB9414247851 +:1034000064F347031370032805D010E023F0FE0394 +:1034100013700228F7D1D8B240F001000AE0000092 +:10342000F00100200302FF0143F0FE00107010784D +:1034300020F0010010700868C2F801008888A2F826 +:10344000050038BD022110F031BD38B50C460978B1 +:10345000222901D2082038BDADF800008DF80220E5 +:1034600068460EF087FD05F0DEFE050003D1212140 +:103470002046FFF74FFE284638BD1CB500208DF8CA +:103480000000CDF80100ADF80500FB4890F82E00D3 +:10349000022801D0012000E000208DF807006846D6 +:1034A0000EF0F0FD002800D0FFDF1CBD00220A80D6 +:1034B000437892B263F3451222F040020A8000780A +:1034C0000C282BD2DFE800F02A06090E1116191C71 +:1034D0001F220C2742F0110009E042F01D00088075 +:1034E0000020704742F0110012E042F0100040F05E +:1034F0000200F4E742F01000F1E742F00100EEE7CD +:1035000042F0010004E042F00200E8E742F002006D +:1035100040F00400E3E742F00400E0E707207047D2 +:103520002DE9FF478AB00025BDF82C6082461C4675 +:1035300091468DF81C50700703D56068FDF789FE31 +:1035400068B9CD4F4FF0010897F82E0058B197F8A1 +:103550002F00022807D16068FDF7C8FE18B11020BF +:103560000EB0BDE8F087300702D5A08980283DD88D +:10357000700705D4B9F1000F02D097F8240098B372 +:10358000E07DC0F300108DF81B00627D0720032151 +:103590005AB3012A2CD0022AE2D0042AE0D18DF8B5 +:1035A0001710F00627D4A27D072022B3012A22D0CB +:1035B000022A23D0042AD3D18DF819108DF8159042 +:1035C000606810B307A9FFF7AAFE0028C8D19DF8CC +:1035D0001C00FF2816D0606850F8011FCDF80F10AE +:1035E0008088ADF8130014E000E001E00720B7E7A1 +:1035F0008DF81780D5E78DF81980DFE702208DF868 +:103600001900DBE743F20220AAE7CDF80F50ADF82E +:103610001350E07B40B9207C30B9607C20B9A07C9D +:1036200010B9E07CC00601D0062099E78DF800A013 +:10363000BDF82C00ADF80200A0680190A0680290CF +:1036400004F10F0001F0A9FC8DF80C00FFF790FECB +:103650008DF80D009DF81C008DF80E008DF81650A9 +:103660008DF81850E07D08A900F00F008DF81A00C1 +:1036700068460FF0E3F905F0D6FD71E7F0B58FB0BD +:1036800000258DF830508DF814508DF834500646D2 +:103690008DF82850019502950395049519B10FC92D +:1036A00001AC84E80F00744CA078052801D00428F0 +:1036B0000CD101986168884200D120B90398E16873 +:1036C000884203D110B108200FB0F0BD207DC006A4 +:1036D00001D51F2700E0FF273B460DAA05A903A837 +:1036E000FFF7AAFD0028EFD1A08AC10702D0C006CB +:1036F00000D4EE273B460AAA0CA901A8FFF79CFDBF +:103700000028E1D19DF81400C00701D00A20DBE7B2 +:10371000A08A410708D4A17D31B19DF828108907FE +:1037200002D043F20120CFE79DF82810C90709D045 +:10373000400707D4208818B144F25061884201D96B +:103740000720C1E78DF818508DF81960BDF8080002 +:10375000ADF81A000198079006A80FF07BF905F064 +:1037600062FD0028B0D18DF820508DF82160BDF8A1 +:103770001000ADF822000398099008A80FF08CF90A +:1037800005F051FD00289FD101AD241D95E80F00E3 +:1037900084E80F00002097E770B586B00D4604005E +:1037A00005D0FDF7A3FD20B1102006B070BD0820A4 +:1037B000FBE72078C107A98802D0FF2902D303E0E4 +:1037C0001F2901D20920F0E7800763D4FFF75CFCD2 +:1037D00038B12178C1F3C100012804D0032802D0F8 +:1037E00005E01320E1E7244890F82400C8B1C80799 +:1037F0004FF001064FF0000502D08DF80F6001E098 +:103800008DF80F50FFF7B4FD8DF800002078694661 +:10381000C0F3C1008DF8010060788DF80250C20835 +:1038200001D00720C1E730B3C20701D08DF8026094 +:10383000820705D59DF8022042F002028DF8022091 +:10384000400705D59DF8020040F004008DF8020005 +:10385000002022780B18C2F38002DA7001EB4002DC +:103860006388D380401CA388C0B253810228F0D360 +:10387000207A78B905E001E0F00100208DF80260BF +:10388000E6E7607A30B9A07A20B9E07A10B9207BF7 +:10389000C00601D0062088E704F1080001F07DFB96 +:1038A0008DF80E0068460EF0F6FC05F0BCFC002812 +:1038B00089D18DF810608DF81150E088ADF81200B4 +:1038C000ADF8145004A80EF039FD05F0ACFC00284A +:1038D00088D12078C00701D0152000E01320FFF721 +:1038E000BDFB002061E72DE9FF470220FD4E8DF86A +:1038F00004000027708EADF80600B84643F20209B6 +:103900004CE001A810F039FA050006D0708EA8B37B +:10391000A6F83280ADF806803EE0039CA07F010748 +:103920002DD504F124000090A28EBDF80800214698 +:1039300004F1360301F0BCFC050005D04D452AD04A +:10394000112D3CD0FFDF3AE0A07F20F00801E07F9E +:10395000420862F3C711A177810861F30000E077A4 +:1039600094F8210000F01F0084F820002078282817 +:1039700026D129212046FFF7CDFB21E014E04007A6 +:103980000AD5BDF8080004F10E0101F01CFB05008A +:103990000DD04D4510D100257F1CFFB2022010F044 +:1039A0002DFA401CB842ACD8052D11D008E0A07FFC +:1039B00020F00400A07703E0112D00D0FFDF0025E8 +:1039C000BDF806007086052D04D0284604B0C8E571 +:1039D000A6F832800020F9E770B50646FFF732FD01 +:1039E000054605F003FE040000D1FFDF6680207865 +:1039F00020F00F00801C20F0F00020302070032009 +:103A0000207295F83E006072BDE8704005F0F1BD8F +:103A10002DE9F04786B0040000D1FFDF2078B14DDA +:103A200020F00F00801C20F0F000703020706068E3 +:103A30000178491F1B2933D2DFE801F0FE32323210 +:103A400055FD320EFDFD42FC32323278FCFCFBFAB1 +:103A500032FCFCF9F8FCFC00C6883046FFF7F2FCAB +:103A60000546304607F045FCE0B16068007A85F80D +:103A70003E0021212846FFF74DFB3046FEF75AFB5A +:103A8000304603F0D7FE3146012014F017F8A87F26 +:103A900020F01000A877FFF726FF002800D0FFDFF6 +:103AA00006B05EE5207820F0F00020302070032082 +:103AB000207266806068007A607205F09AFDD8E72F +:103AC000C5882846FFF7BEFC00B9FFDF60680079B3 +:103AD000012800D0FFDF6068017A06B02846BDE803 +:103AE000F04707F0EBBDC6883046FFF7ABFC05009A +:103AF00000D1FFDF05F07DFD606831460089288137 +:103B000060684089688160688089A881012013F01D +:103B1000D5FF0020A875A87F00F003000228BFD1C0 +:103B2000FFF7E1FE0028BBD0FFDFB9E7007928B13D +:103B30000228B5D03C28B3D0FFDFB1E705F059FD2E +:103B40006668B6F806A0307A361D012806D0687E71 +:103B5000814605F0D4FA070003D101E0E878F7E7E1 +:103B6000FFDF00220221504610F097F9040000D137 +:103B7000FFDF22212046FFF7CDFA3079012800D05F +:103B80000220A17F804668F30101A177308B20815C +:103B9000708B6081B08BA08184F822908DF80880B2 +:103BA000B8680090F86801906A460321504610F00A +:103BB00074F900B9FFDFB888ADF81000B8788DF857 +:103BC000120004AA0521504610F067F900B9FFDF82 +:103BD000B888ADF80C00F8788DF80E0003AA04211F +:103BE000504610F05AF900B9FFDF062106F1120025 +:103BF0000DF00EF940B37079800700D5FFDF7179C1 +:103C0000E07D61F34700E075D6F80600A061708999 +:103C1000A083062106F10C000DF0FAF8F0B195F83A +:103C200025004108607861F3470006E041E039E093 +:103C300071E059E04EE02FE043E06070D5F82600D7 +:103C4000C4F80200688D12E0E07D20F0FE00801CC8 +:103C5000E075D6F81200A061F08AD9E7607820F00C +:103C6000FE00801C6070F068C4F80200308AE080BA +:103C7000B8F1010F04D0B8F1020F05D0FFDF0FE754 +:103C80000320FFF7D3F90BE7287E122800D0FFDFCF +:103C90001120FFF7E3F903E706B02046BDE8F0473F +:103CA00001F092BD05F0A5FC15F8300F40F00200C0 +:103CB00005E005F09EFC15F8300F40F00400287078 +:103CC000EEE6287E13280AD01528D8D15FF016001A +:103CD000FFF7C4F906B0BDE8F04705F08ABC142030 +:103CE000F6E70000F0010020A978052909D0042991 +:103CF000C5D105F07EFC022006B0BDE8F047FFF715 +:103D000095B900790028BAD0E87801F02DF905F0CE +:103D100070FC0320F0E7287E122802D1687E01F0B3 +:103D200023F91120D4E72DE9F05F054600784FF024 +:103D300000080009DFF8B8A891460C46464601285D +:103D40006ED002286DD007280BD00A286AD0FFDF7A +:103D5000A9F8006014B1A4F8008066800020BDE8D6 +:103D6000F09F6968012704F108000B784FF0020BFF +:103D70005B1F4FF6FF721B2B7ED2DFE803F0647DE2 +:103D80007D7D0E7D7D7D7D7D7D217D7D7D2BFDFC81 +:103D9000FBFA7D14D2F9E7F8F700C8884FF0120853 +:103DA000102621469AE14FF01C080A26BCB38888E9 +:103DB000A0806868807920726868C0796072C7E7FF +:103DC0004FF01B08142654B30320207268688088C3 +:103DD000A080BDE70A793C2ABAD00D1D4FF010082B +:103DE0002C26E4B16988A180298B6182298B2182EC +:103DF000698BA182A98BE1826B790246A91D1846C5 +:103E0000FFF7EFFA2979002001290CD084F80FB0D0 +:103E1000FF212176E06120626062A06298E70FE0F6 +:103E20003BE15EE199E1E77320760AF1040090E856 +:103E30000E00DAF81000C4E90930C4E9071287E778 +:103E4000A9F800608AE72C264FF01D08002CF7D057 +:103E5000A28005460F1D897B008861F30000288041 +:103E6000B97A490861F341002880B97A890861F379 +:103E700082002880B97A00E00CE1C90861F3C30030 +:103E80002880B97AAA1C0911491C61F3041000F0BA +:103E90007F0028807878B91CFFF7A3FA387D05F1F8 +:103EA000090207F11501FFF79CFA387B01F0A9F828 +:103EB0002874787B01F0A5F86874F87EA874787A85 +:103EC000E874387F2875B87B6875388AE882DAF834 +:103ED0001C10A961B97A504697F808A0C1F34111A6 +:103EE000012904D0008C504503D2824609E0FFDF4F +:103EF00010E0022903D0288820F0600009E0504536 +:103F000004D1288820F06000403002E0288840F08A +:103F100060002880A4F824A0524607F11D01A8697A +:103F20009BE011264FF02008002C89D0A280686801 +:103F300004F10A02007920726868007B6072696887 +:103F40008B1D48791946FFF74CFA01E70A264FF016 +:103F50002108002CE9D08888A080686880792072C8 +:103F60006868C07960729AF8301006E078E06BE01B +:103F700052E07FE019E003E03AE021F00401A6E01E +:103F80000B264FF02208002CCFD0C888A08068688C +:103F9000007920726868007A01F033F8607268680E +:103FA000407A01F02EF8A072D2E61C264FF02608C7 +:103FB000002CBAD0A2806868407960726868007A84 +:103FC000A0720AF1040090E80E00DAF81000C4E9CB +:103FD0000530C4E90312686800793C2803D04328FF +:103FE00003D0FFDFB4E62772B2E684F808B0AFE68C +:103FF00010264FF02408002C97D08888A08068688D +:10400000807920816868807A608168680089A081F1 +:1040100068688089E0819BE610264FF02308002C19 +:1040200098D08888A0806868C088208168680089E6 +:10403000608168684089A08168688089E0819AF819 +:10404000301021F0020142E030264FF02508002C0C +:104050009AD0A2806968282249680AF0EEF977E6CA +:104060002A264FF02F08002C8ED0A28069682222C9 +:10407000091DF2E714264FF01B08002C84D0A28003 +:10408000686800790128B0D02772DAE90710C4E91E +:1040900003105DE64A46214660E0287A012803D0F5 +:1040A000022817D0FFDF53E610264FF01F08002C20 +:1040B000A2D06888A080A8892081E8896081288AA8 +:1040C000A081688AE0819AF8301021F001018AF815 +:1040D00030103DE64FF012081026688800F07EFF91 +:1040E00036E6287AC8B3012838D0022836D003280B +:1040F00001D0FFDF2CE609264FF01108002C8FD0ED +:104100006F883846FFF79EF990F822A0A780687A5A +:104110002072042138460FF0DBFE052138460FF0EF +:10412000D7FE002138460FF0D3FE012138460FF0AC +:10413000CFFE032138460FF0CBFE022138460FF0A8 +:10414000C7FE062138460FF0C3FE072138460FF0A0 +:10415000BFFE504600F008FFFAE5FFE72846BDE83D +:10416000F05F01F0BBBC70B5012803D0052800D07A +:10417000FFDF70BD8DB22846FFF764F9040000D15F +:10418000FFDF20782128F4D005F030FA80B10178E3 +:1041900021F00F01891C21F0F00110310170022182 +:1041A000017245800020A075BDE8704005F021BA7D +:1041B00021462846BDE870401322FFF746B92DE995 +:1041C000F04116460C00804600D1FFDF307820F029 +:1041D0000F00801C20F0F000103030702078012893 +:1041E00004D0022818D0FFDFBDE8F0814046FFF779 +:1041F00029F9050000D1FFDF0320A87505F0F9F9C2 +:1042000094E80F00083686E80F00F94810F8301FD0 +:1042100041F001010170E7E74046FFF713F905009F +:1042200000D1FFDFA1884FF6FF700027814202D145 +:10423000E288824203D0814201D1E08840B105F09A +:10424000D8F994E80F00083686E80F00AF75CBE781 +:10425000A87D0128C8D178230022414613F084FBB1 +:104260000220A875C0E738B50C4624285CD008DCCD +:1042700020280FD0212825D022284BD0232806D152 +:104280004CE0252841D0262832D03F2851D00725A0 +:10429000284638BD0021052013F0E6FB08B11120A7 +:1042A00038BDA01C0EF0E1FA04F0BDFF0500EFD10F +:1042B000002208231146052013F056FB0528E7D0FD +:1042C000FFDFE5E76068FDF708F808B1102038BDAA +:1042D000618820886A460EF071FD04F0A4FF050095 +:1042E000D6D160680028D3D0BDF800100180CFE798 +:1042F000206820B1FCF7FAFF08B11025C8E7204676 +:104300000EF03BFE1DE00546C2E7A17820880EF0C6 +:1043100086FD16E0086801F08DFEF4E7087800F0ED +:1043200001000DF0B9FD0CE0618820880EF0C1FCA1 +:1043300007E0087800F001008DF8000068460EF0F4 +:10434000DFF804F070FFDEE770B505460C4608465E +:10435000FCF7A5FF08B1102070BD202D07D0212D3E +:104360000DD0222D0BD0252D09D0072070BD20881F +:10437000A11C0DF065FEBDE8704004F054BF06209E +:1043800070BD9B482530704708B5342200219848FD +:104390000AF07DF80120FEF749FE1120FEF75EFECF +:1043A00093496846263105F0B7F891489DF80020FA +:1043B00010F8251F62F3470121F00101017000216F +:1043C00041724FF46171A0F8071002218172FEF76B +:1043D0008FFE00B1FFDFFDF705F801F0BEF908BD63 +:1043E00010B50C464022002120460AF050F8A07F6C +:1043F00020F00300A077202020700020A07584F812 +:10440000230010BD70472DE9FC410746FCF721FF52 +:1044100010B11020BDE8FC81754E06F12501D6F8DB +:1044200025000090B6F82950ADF8045096F82B40BE +:104430008DF806403846FEF7BDFF0028EAD1FEF7AA +:1044400057FE0028E6D0009946F8251FB580B471C4 +:10445000E0E710B50446FCF722FF08B1102010BDBC +:1044600063486349224690F8250026314008FEF74C +:10447000B8FF002010BD3EB504460D460846FCF7C7 +:104480000EFF08B110203EBD14B143F204003EBD42 +:1044900057488078052803D0042801D008203EBD65 +:1044A000694602A80AF012FC2A4669469DF80800EF +:1044B000FEF797FF00203EBDFEB50D4604004FF00D +:1044C000000712D00822FEF79FFE002812D1002616 +:1044D00009E000BF54F826006946FEF720FF0028D7 +:1044E00008D1761CF6B2AE42F4D30DF01CFC10B12C +:1044F00043F20320FEBD3E4E86F824700CB3002725 +:104500001BE000BF54F8270002A9FEF708FF00B126 +:10451000FFDF9DF808008DF8000054F8270050F8E0 +:10452000011FCDF801108088ADF8050068460DF038 +:104530001FFC00B1FFDF7F1CFFB2AF42E2D386F861 +:1045400024500020FEBD2DE9F0418AB01546884672 +:1045500004001ED00F4608222946FEF755FE00280B +:1045600011D1002613E000BF54F826006946103030 +:1045700000F01FFD002806D13FB157F82600FCF7D8 +:1045800068FE10B110200AB02EE6761CF6B2AE42DC +:10459000EAD3681EC6B217E0701CC7B212E000BFB3 +:1045A00054F82600017C4A0854F827100B7CB2EB23 +:1045B000530F05D106221130113109F011FF50B10E +:1045C0007F1CFFB2AF42EBD3761EF6B2E4D2464672 +:1045D00024B1012003E043F20520D4E700200DF0D0 +:1045E000ECFB10B90DF0F5FB20B143F20420CAE753 +:1045F000F001002064B300270DF1170826E000BF8A +:1046000054F827006946103000F0D3FC00B1FFDFFA +:1046100054F82700102250F8111FCDF8011080889F +:10462000ADF8050054F827100DF1070009F005FF5B +:1046300096B156F827101022404609F0FEFE684653 +:104640000DF07BFB00B1FFDF7F1CFFB2AF42D7D381 +:10465000FEF713FF002096E7404601F0DFFCEEE78F +:1046600030B585B00446FDF7BDF830B906200FF02F +:10467000C5FB10B1062005B030BD2046FCF7E9FDB2 +:1046800018B96068FCF732FE08B11020F3E76088C3 +:104690004AF2B811884206D82078F94D28B101288D +:1046A00006D0022804D00720E5E7FEF721FD18E038 +:1046B0006078022804D0032802D043F20220DAE70F +:1046C00085F82F00C1B200200090ADF80400022947 +:1046D0002CD0032927D0FFDF68460DF009FC04F039 +:1046E000A2FD0028C7D1606801F08BFC207858B18A +:1046F00001208DF800000DF1010001F08FFC6846EB +:104700000EF0FDFB00B1FFDF207885F82E00FEF7EC +:10471000B4FE608860B1A88580B20DF046FB00B1A0 +:10472000FFDF0020A7E78DF80500D5E74020FAE776 +:104730004FF46170EFE710B50446FCF7B0FD20B907 +:10474000606838B1FCF7C9FD08B1102010BD606881 +:1047500001F064FCCA4830F82C1F6180C178617098 +:1047600080782070002010BD2DE9F843144689465A +:104770000646FCF794FDA0B94846FCF7B7FD80B9A2 +:104780002046FCF7B3FD60B9BD4DA878012800D1E3 +:104790003CB13178FF2906D049B143F20400BDE8AD +:1047A000F8831020FBE7012801D00420F7E7CCB301 +:1047B000052811D004280FD069462046FEF776FE62 +:1047C0000028ECD1217D49B1012909D0022909D065 +:1047D000032909D00720E2E70820E0E7024604E0C9 +:1047E000012202E0022200E003228046234617460F +:1047F00000200099FEF794FE0028D0D1A0892880DF +:10480000A07BE875BDF80000A882AF75BDF8001068 +:10481000090701D5A18931B1A1892980C00704D038 +:10482000032003E006E08021F7E70220FEF7FEFB0D +:1048300086F800804946BDE8F8430020FEF71EBF19 +:104840007CB58F4C05460E46A078022803D003287D +:1048500001D008207CBD15B143F204007CBD0720C7 +:104860000FF0D4FA10B9A078032806D0FEF70CFC9C +:1048700028B1A078032804D009E012207CBD1320C1 +:104880007CBD304600F053FB0028F9D1E670FEF7FE +:104890006FFD0AF058F901208DF800008DF8010035 +:1048A0008DF802502088ADF80400E07D8DF80600F8 +:1048B00068460EF0C1F904F0B6FC0028E0D1A078FB +:1048C000032805D05FF00400FEF7B0FB00207CBD9C +:1048D000E07800F03CFB0520F6E71CB510B143F290 +:1048E00004001CBD664CA078042803D0052801D024 +:1048F00008201CBD00208DF8000001218DF801105A +:104900008DF8020068460EF097F904F08CFC002840 +:10491000EFD1A078052805D05FF00200FEF786FBF6 +:1049200000201CBDE07800F01FFB0320F6E72DE916 +:10493000FC4180460E4603250846FCF7D7FC0028BC +:1049400066D14046FEF77EFD040004D02078222880 +:1049500004D208205EE543F202005BE5A07F00F090 +:1049600003073EB1012F0CD000203146FEF727FC93 +:104970000500EFD1012F06D0022F1AD0FFDF284605 +:1049800048E50120F1E7A07D3146022801D011B1B0 +:1049900007E011203EE56846FCF758FE0028D9D113 +:1049A0006946404606F04DFE0500E8D10120A0759D +:1049B000E5E7A07D032804D1314890F83000C00716 +:1049C00001D02EB30EE026B1A07F40071ED40021F7 +:1049D00000E00121404606F054FE0500CFD1A0754D +:1049E000002ECCD03146404600F0EDFA05461128A5 +:1049F000C5D1A07F4107C2D4316844F80E1F716849 +:104A0000616040F0040020740025B8E71125B6E786 +:104A10001020FFE470B50C460546FEF713FD0100BB +:104A200005D022462846BDE87040FEF70EBD43F291 +:104A3000020070BD10B5012807D1114B9B78012BE6 +:104A400000D011B143F2040010BD0DF0E0F9BDE853 +:104A5000104004F0E8BB012300F090BA00231A468E +:104A6000194600F08BBA70B506460C460846FCF7AE +:104A7000F0FB18B92068FCF712FC18B1102070BDCB +:104A8000F0010020F84D2A7E112A04D0132A00D309 +:104A90003EB10820F3E721463046FEF77DFE60B1C7 +:104AA000EDE70920132A0DD0142A0BD0A188FF2985 +:104AB000E5D31520FEF7D2FA0020D4E90012C5E9AB +:104AC0000712DCE7A1881F29D9D31320F2E71CB510 +:104AD000E548007E132801D208201CBD00208DF877 +:104AE000000068460DF02AFC04F09DFB0028F4D17C +:104AF0001120FEF7B3FA00201CBD2DE9F04FDFF8BE +:104B000068A3814691B09AF818009B4615460C465A +:104B1000132803D3FFF7DBFF00281FD12046FCF743 +:104B200098FBE8BB2846FCF794FBC8BB20784FF005 +:104B30000107C0074FF0000102D08DF83A7001E084 +:104B40008DF83A1020788846C0F3C1008DF8000037 +:104B500060788DF80910C10803D0072011B0BDE8B6 +:104B6000F08FB0B3C10701D08DF80970810705D56A +:104B70009DF8091041F002018DF80910400705D594 +:104B80009DF8090040F004008DF809009DF8090027 +:104B9000810703D540F001008DF80900002000E0F6 +:104BA00015E06E4606EB400162884A81401CA288EF +:104BB000C0B20A820328F5D32078C0F3C1000128CF +:104BC00025D0032823D04846FCF743FB28B110200A +:104BD000C4E7FFE78DF80970D8E799F800004008AE +:104BE00008D0012809D0022807D0032805D043F2B5 +:104BF0000220B3E78DF8028001E08DF8027048468C +:104C000050F8011FCDF803108088ADF80700FEF7BB +:104C1000AFFB8DF801000021424606EB41002B88D6 +:104C2000C3826B888383AB884384EB880385491CEC +:104C3000C285C9B282860329EFD3E088ADF83C0073 +:104C400068460DF053FC002887D19AF818005546A5 +:104C5000112801D0082081E706200FF0D7F838B1DD +:104C60002078C0F3C100012804D0032802D006E058 +:104C7000122073E795F8240000283FF46EAFFEF78A +:104C800003FA022801D2132068E7584600F04FF9D2 +:104C900000289DD185F819B068460DF06DFD04F02F +:104CA000C2FA040094D1687E00F051F91220FEF798 +:104CB000D5F9204652E770B56B4D287E122801D0F9 +:104CC0000820DCE60DF05BFD04F0ADFA040005D130 +:104CD000687E00F049F91120FEF7C0F92046CEE6C3 +:104CE00070B5064615460C460846FCF7D8FA18B9C2 +:104CF0002846FCF7D4FA08B11020C0E62A4621461F +:104D000030460EF03BF804F08EFA0028F5D12178F9 +:104D10007F29F2D10520B2E67CB505460C4608464F +:104D2000FCF797FA08B110207CBD2846FEF78AFBF5 +:104D300020B10078222804D208207CBD43F2020072 +:104D40007CBD494890F83000400701D511207CBD5A +:104D50002078C00802D16078C00801D007207CBD4F +:104D6000ADF8005020788DF8020060788DF80300CF +:104D70000220ADF8040068460CF03BFE04F053FA44 +:104D80007CBD70B586B014460D460646FEF75AFB4C +:104D900028B10078222805D2082006B06FE643F239 +:104DA0000200FAE72846FCF7A1FA20B944B12046F0 +:104DB000FCF793FA08B11020EFE700202060A080F4 +:104DC000294890F83000800701D51120E5E703A9B4 +:104DD00030460CF05EFE10B104F025FADDE7ADF8C8 +:104DE0000060BDF81400ADF80200BDF81600ADF883 +:104DF0000400BDF81000BDF81210ADF80600ADF8C3 +:104E000008107DB1298809B1ADF80610698809B18B +:104E1000ADF80210A98809B1ADF80810E98809B108 +:104E2000ADF80410DCB1BDF80610814201D9081AB2 +:104E30002080BDF80210BDF81400814201D9081A83 +:104E40006080BDF80800BDF80410BDF816200144CC +:104E5000BDF812001044814201D9081AA0806846AA +:104E60000CF0D5FEB8E70000F00100201CB56C493D +:104E70000968CDE9001068460DF03AFB04F0D3F95B +:104E80001CBD1CB500200090019068460DF030FB61 +:104E900004F0C9F91CBD70B505460C460846FCF780 +:104EA000FEF908B11020EAE5214628460DF012F976 +:104EB000BDE8704004F0B7B93EB505460C4608465B +:104EC000FCF7EDF908B110203EBD002000900190E4 +:104ED0000290ADF800502089ADF8080020788DF8D8 +:104EE0000200606801902089ADF808006089ADF883 +:104EF0000A0068460DF000F904F095F93EBD0EB5C4 +:104F0000ADF800000020019068460DF0F5F804F0BF +:104F10008AF90EBD10800888508048889080C88823 +:104F200010818888D080002050819081704710B512 +:104F3000044604F0E4F830B1407830B1204604F083 +:104F4000FCFB002010BD052010BD122010BD10B5C7 +:104F500004F0D5F8040000D1FFDF607800B9FFDF6E +:104F60006078401E607010BD10B504F0C8F80400F1 +:104F700000D1FFDF6078401C607010BD1CB5ADF83B +:104F800000008DF802308DF803108DF8042068467B +:104F90000DF0B7FE04F047F91CBD0CB521A2D2E913 +:104FA0000012CDE900120079694601EB501000783B +:104FB0000CBD0278520804D0012A02D043F202202C +:104FC0007047FEF7ACB91FB56A46FFF7A3FF684606 +:104FD0000DF008FC04F027F904B010BD70B50C000A +:104FE00006460DD0FEF72EFA050000D1FFDFA680A1 +:104FF00028892081288960816889A081A889E08129 +:105000003DE500B540B1012805D0022803D00328B2 +:1050100004D0FFDF002000BDFF2000BD042000BD44 +:1050200014610200070605040302010010B50446DE +:10503000FCF70FF908B1102010BD2078C0F3021062 +:10504000042807D86078072804D3A178102901D84C +:10505000814201D2072010BDE078410706D42179B2 +:105060004A0703D4000701D4080701D5062010BD64 +:10507000002010BD10B513785C08837F64F3C7135C +:10508000837713789C08C37F64F30003C377107899 +:10509000C309487863F34100487013781C090B7802 +:1050A00064F347130B701378DB0863F30000487058 +:1050B0005078487110BD10B5C4780B7864F30003C4 +:1050C0000B70C478640864F341030B70C478A408BF +:1050D00064F382030B70C478E40864F3C3030B70B9 +:1050E0000379117863F30001117003795B0863F3AE +:1050F0004101117003799B0863F3820111700079FB +:10510000C00860F3C301117010BD70B514460D46A0 +:10511000064604F06BFA80B10178182221F00F01E5 +:10512000891C21F0F001A03100F8081B214609F08C +:1051300084F9BDE8704004F05CBA29463046BDE809 +:1051400070401322FEF781B92DE9F047064608A802 +:10515000904690E8300489461F46142200212846D4 +:1051600009F095F90021CAF80010B8F1000F03D03A +:10517000B9F1000F03D114E03878C00711D02068CE +:10518000FCF78DF8C0BBB8F1000F07D120681230D2 +:1051900028602068143068602068A8602168CAF818 +:1051A00000103878800724D56068FCF796F818BBA3 +:1051B000B9F1000F21D0FFF7E4F80168C6F86811D3 +:1051C0008188A6F86C11807986F86E0101F013FDD4 +:1051D000F94FEF60626862B196F8680106F26911F2 +:1051E00040081032FEF7FDF810223946606809F0D9 +:1051F00024F90020BDE8F08706E0606820B1E8608F +:105200006068C6F86401F4E71020F3E730B505469E +:1052100008780C4620F00F00401C20F0F0011031FF +:1052200021700020607095F8230030B104280FD061 +:10523000052811D0062814D0FFDF20780121B1EB1A +:10524000101F04D295F8200000F01F00607030BDE0 +:1052500021F0F000203002E021F0F000303020702A +:10526000EBE721F0F0004030F9E7F0B591B002270C +:1052700015460C4606463A46ADF80870092103ABC0 +:1052800005F063F80490002810D004208DF8040085 +:105290008DF80170E034099605948DF818500AA92C +:1052A000684610F0FCFA00B1FFDF012011B0F0BD3C +:1052B00010B588B00C460A99ADF80000CBB118685B +:1052C000CDF80200D3F80400CDF80600ADF80A20AE +:1052D000102203A809F0B1F868460DF0F3FA03F0C4 +:1052E000A2FF002803D1A17F41F01001A17708B0EF +:1052F00010BD0020CDF80200E6E72DE9F84F064684 +:10530000808A0D4680B28246FEF79CF804463078CB +:10531000DFF8A48200274FF00209A8F120080F2827 +:1053200070D2DFE800F06FF23708387D8CC8F1F0FA +:10533000EFF35FF3F300A07F00F00300022809D031 +:105340005FF0000080F0010150460EF0AFFD050057 +:1053500003D101E00120F5E7FFDF98F85C10C907F1 +:1053600002D0D8F860000BE0032105F11D0012F017 +:10537000BEF8D5F81D009149B0FBF1F201FB120017 +:10538000C5F81D0070686867B068A8672078252890 +:1053900000D0FFDFCAE0A07F00F00300022809D0A0 +:1053A0005FF0000080F0010150460EF07FFD060026 +:1053B00003D101E00120F5E7FFDF3078810702D556 +:1053C0002178252904D040F001003070BDE8F88F25 +:1053D00085F80090307F287106F11D002D36C5E953 +:1053E0000206F3E7A07F00F00300022808D00020A7 +:1053F00080F0010150460EF059FD040004D102E096 +:105400000120F5E7A7E1FFDF2078C10604D50720DA +:1054100028703D346C60D9E740F008002070D5E773 +:10542000E07F000700D5FFDF307CB28800F0010389 +:1054300001B05046BDE8F04F092106F064B804B948 +:10544000FFDF716821B1102204F1240008F0F5FF9C +:1054500028212046FDF75EFEA07F00F00300022811 +:105460000ED104F12400002300901A462146504634 +:10547000FFF71EFF112807D029212046FDF74AFE1D +:10548000307A84F82000A1E7A07F000700D5FFDF75 +:1054900014F81E0F40F008002070E782A761E76152 +:1054A000C109607861F34100014660F382016170D7 +:1054B000307AE0708AE7A07F00F00300022809D06C +:1054C0005FF0000080F0010150460EF0EFFC040098 +:1054D00003D101E00120F5E7FFDF022104F185009F +:1054E00012F005F80420287004F5B4706860B4F870 +:1054F00085002882304810387C346C61C5E9028010 +:1055000064E703E024E15BE02DE015E0A07F00F01C +:105510000300022807D0002080F0010150460EF061 +:10552000C5FC18B901E00120F6E7FFDF324621464D +:105530005046BDE8F84FE8E504B9FFDF20782128A0 +:10554000A1D93079012803D1E07F40F00800E0774D +:10555000324621465046FFF7D8FD2046BDE8F84FB9 +:105560002321FDF7D7BD3279AA8005F1080309216F +:10557000504604F0EAFEE86010B10520287025E7E7 +:10558000A07F00F00300022808D0002080F0010175 +:1055900050460EF08BFC040003D101E00120F5E73A +:1055A000FFDF04F1620102231022081F0EF005FB49 +:1055B00007703179417009E75002002040420F0026 +:1055C000A07F00F00300022808D0002080F0010135 +:1055D00050460EF06BFC050003D101E00120F5E719 +:1055E000FFDF95F8840000F0030001287AD1A07F46 +:1055F00000F00307E07F10F0010602D0022F04D173 +:1056000033E095F8A000C0072BD0D5F8601121B386 +:1056100095F88320087C62F387000874A17FCA098B +:10562000D5F8601162F341000874D5F8601166F393 +:1056300000000874AEB1D5F86001102204F1240115 +:10564000883508F0FAFE287E40F001002876287898 +:1056500020F0010005F8880900E016B1022F04D0FF +:105660002DE095F88800C00727D0D5F85C1121B34C +:1056700095F88320087C62F387000874A17FCA092B +:10568000D5F85C1162F341000874D5F85C1166F33B +:10569000000008748EB1D5F85C01102204F12401D9 +:1056A000883508F0CAFE287840F0010005F8180B8C +:1056B000287820F0010005F8A009022F44D000202E +:1056C00000EB400005EBC00090F88800800709D58A +:1056D00095F87C00D5F86421400805F17D01103271 +:1056E000FDF77FFE8DF8009095F884006A4600F083 +:1056F00003008DF8010095F888108DF8021095F8D8 +:10570000A0008DF803002146504601F05DFA207894 +:10571000252805D0212807D0FFDF2078222803D9AB +:1057200022212046FDF7F6FCA07F00F003000228AE +:105730000CD0002080F0010150460EF0C9FB00287B +:105740003FF44FAEFFDF41E60120B9E70120F1E76A +:10575000706847703AE6FFDF38E670B5FE4C00250A +:1057600084F85C50256610F066F804F110012046BC +:1057700003F0F8FE84F8305070BD70B50D46FDF7AB +:1057800061FE040000D1FFDF4FF4B872002128460B +:1057900008F07DFE04F124002861A07F00F00300E2 +:1057A000022809D05FF0010105F1E00010F044F893 +:1057B000002800D0FFDF70BD0221F5E70A46014650 +:1057C00002F1E00010F059B870B50546406886B0A7 +:1057D00001780A2906D00D2933D00E292FD0FFDFFA +:1057E00006B070BD86883046FDF72CFE040000D15F +:1057F000FFDF20782128F3D028281BD168680221F8 +:105800000E3001F0D6F9A8B168680821801D01F0BA +:10581000D0F978B104F1240130460DF00FFA03F00D +:1058200002FD00B1FFDF06B02046BDE8704029212F +:10583000FDF770BC06B0BDE8704003F0DABE012190 +:1058400001726868C6883046FDF7FCFD040000D18F +:10585000FFDFA07F00F00301022902D120F0100039 +:10586000A077207821280AD06868017A09B10079E8 +:1058700080B1A07F00F00300022862D0FFDFA07F8C +:1058800000F003000228ABD1FEF72DF80028A7D0C6 +:10589000FFDFA5E703F0ADFEA17F08062BD5E07F73 +:1058A000C00705D094F8200000F01F00102820D079 +:1058B0005FF0050084F82300207829281DD02428D3 +:1058C000DDD13146042012F0F9F822212046FDF7FF +:1058D00021FCA07F00F00300022830D05FF0000020 +:1058E00080F0010130460EF0F3FA0028C7D0FFDF48 +:1058F000C5E70620DEE70420DCE701F0030002280C +:1059000008D0002080F0010130460EF0CFFA0500EB +:1059100003D101E00120F5E7FFDF25212046FDF757 +:10592000F9FB03208DF80000694605F1E0000FF057 +:105930009BFF0228A3D00028A1D0FFDF9FE7012012 +:10594000CEE703F056FE9AE72DE9F04387B099467B +:10595000164688460746FDF775FD04004BD02078B3 +:10596000222848D3232846D0E07F000743D4A07FD5 +:1059700000F00300022809D05FF0000080F0010170 +:1059800038460EF093FA050002D00CE00120F5E74E +:10599000A07F00F00300022805D001210022384634 +:1059A0000EF07BFA05466946284601F034F9009866 +:1059B00000B9FFDF45B10098E03505612078222865 +:1059C00006D0242804D007E000990020086103E0F5 +:1059D00025212046FDF79EFB00980121417047627A +:1059E000868001A9C0E902890FF059FF022802D080 +:1059F000002800D0FFDF07B0BDE8F08370B586B0A7 +:105A00000546FDF71FFD017822291ED9807F00F091 +:105A10000300022808D0002080F0010128460EF083 +:105A200045FA04002FD101E00120F5E7FFDF2AE06D +:105A3000B4F85E0004F1620630440178427829B17E +:105A400021462846FFF711FCB0B9C9E6ADF804209D +:105A50000921284602AB04F078FC03900028F4D01A +:105A600005208DF80000694604F1E0000FF0FCFE0F +:105A7000022801D000B1FFDF02231022314604F1D9 +:105A80005E000EF0D0F8B4F860000028D0D1A7E690 +:105A900010B586B00446FDF7D5FC017822291BD944 +:105AA000807F00F00300022808D0002080F0010170 +:105AB00020460EF0FBF9040003D101E00120F5E7D8 +:105AC000FFDF06208DF80000694604F1E0000FF0CA +:105AD000CBFE002800D0FFDF06B010BD2DE9F05F3F +:105AE00005460C4600270078904601093E4604F121 +:105AF000080BBA4602297DD0072902D00A2909D10C +:105B000046E0686801780A2905D00D2930D00E29B1 +:105B10002ED0FFDFBBE114271C26002C6BD0808821 +:105B2000A080FDF78FFC5FEA000900D1FFDF99F844 +:105B300017005A46400809F11801FDF752FC686841 +:105B4000C0892082696851F8060FC4F812004868BD +:105B5000C4F81600A07E01E03002002020F006000C +:105B600040F00100A07699F81E0040F020014DE0C1 +:105B70001A270A26002CD1D0C088A080FDF762FC2D +:105B8000050000D1FFDF59462846FFF73FFB7EE1C5 +:105B90000CB1A88BA080287A0B287DD006DC0128C8 +:105BA0007BD0022808D0032804D135E00D2875D019 +:105BB0000E2874D0FFDF6AE11E270926002CADD025 +:105BC000A088FDF73FFC5FEA000900D1FFDF287BDA +:105BD00000F003000128207A1BD020F00100207281 +:105BE000297B890861F341002072297BC90861F390 +:105BF000820001E041E1F2E02072297B090961F3B2 +:105C0000C300207299F81E0040F0400189F81E1070 +:105C10003DE140F00100E2E713270D26002CAAD059 +:105C2000A088FDF70FFC8146807F00F0030002286A +:105C300008D0002080F00101A0880EF037F905009F +:105C400003D101E00120F5E7FFDF99F81E0000F025 +:105C50000302022A50D0686F817801F00301012904 +:105C6000217A4BD021F00101217283789B0863F3E4 +:105C7000410121728378DB0863F38201217283780A +:105C80001B0963F3C3012172037863F306112172C8 +:105C9000437863F3C71103E061E0A9E090E0A1E07D +:105CA000217284F809A0C178A172022A29D0027950 +:105CB000E17A62F30001E1720279520862F3410174 +:105CC000E1720279920862F38201E1720279D208EC +:105CD00062F3C301E1724279217B62F30001217317 +:105CE0004279520862F3410121734279920862F3CA +:105CF00082012173407928E0A86FADE741F00101EE +:105D0000B2E74279E17A62F30001E1724279520826 +:105D100062F34101E1724279920862F38201E17219 +:105D20004279D20862F3C301E1720279217B62F306 +:105D3000000121730279520862F341012173027953 +:105D4000920862F3820121730079C00860F3C301F5 +:105D5000217399F80000232831D9262140E0182723 +:105D60001026E4B3A088FDF76DFB8346807F00F02A +:105D70000300022809D0002080F00101A0880EF065 +:105D800095F85FEA000903D101E00120F4E7FFDFA5 +:105D9000E868A06099F8000040F0040189F800105C +:105DA00099F80100800708D5012020739BF80000B6 +:105DB00023286CD92721584651E084F80CA066E0CE +:105DC00015270F265CB1A088FDF73CFB8146062213 +:105DD0005946E86808F0C7FB0120A073A0E041E045 +:105DE00048463CE016270926E4B3287B20724EE0A3 +:105DF000287B19270E26ACB3C4F808A0A4F80CA081 +:105E0000012807D0022805D0032805D0042803D094 +:105E1000FFDF0DE0207207E0697B042801F00F012D +:105E200041F0800121721ED0607A20F00300607280 +:105E3000A088FDF707FB05460078212827D02328F6 +:105E400000D0FFDFA87F00F00300022813D000205D +:105E500080F00101A0880EF03BF822212846FDF7D2 +:105E600059F914E004E0607A20F00300401CDEE7FA +:105E7000A8F8006010E00120EAE70CB16888A08073 +:105E8000287A68B301280AD002284FD0FFDFA8F88B +:105E900000600CB1278066800020BDE8F09F1527C8 +:105EA0000F26002CE4D0A088FDF7CCFA807F00F00C +:105EB0000300022808D0002080F00101A0880DF026 +:105EC000F5FF050003D101E00120F5E7FFDFD5F87C +:105ED0001D000622594608F046FB84F80EA0D6E7BE +:105EE00017270926002CC3D0A088FDF7ABFA8146FE +:105EF000807F00F00300022808D0002080F001011C +:105F0000A0880DF0D3FF050003D101E00120F5E7E3 +:105F1000FFDF6878800701D5022000E001202072B1 +:105F200099F800002328B2D9272159E719270E260E +:105F3000002C9DD0A088FDF785FA5FEA000900D10A +:105F4000FFDFC4F808A0A4F80CA084F808A0A07A89 +:105F500040F00300A07299F81E10C90961F3820095 +:105F6000A07299F81F2099F81E1012EAD11F05D0CF +:105F700099F8201001F01F0110292BD020F0080003 +:105F8000A07299F81F10607A61F3C3006072697A99 +:105F900001F003010129A2D140F00400607299F8D8 +:105FA0001E0000F003000228E87A16D0217B60F37F +:105FB00000012173AA7A607B62F300006073EA7AC1 +:105FC000520862F341012173A97A490861F3410043 +:105FD00060735CE740F00800D2E7617B60F300018A +:105FE0006173AA7A207B62F300002073EA7A520878 +:105FF00062F341016173A97A490861F3410020739A +:1060000045E710B5FE4C30B10146102204F12000E6 +:1060100008F013FA012084F8300010BD10B50446D2 +:1060200000F0E9FDF64920461022BDE8104020317D +:1060300008F003BA70B5F24D06004FF0000413D01B +:10604000FBF707F908B110240CE00621304608F0F0 +:1060500071FA411C05D028665FF0010085F85C00EC +:1060600000E00724204670BD0020F7E7007810F01C +:106070000F0204D0012A05D0022A0CD110E0000939 +:1060800009D10AE00009012807D0022805D0032819 +:1060900003D0042801D00720704708700020704703 +:1060A0000620704705282AD2DFE800F003070F1703 +:1060B0001F00087820F0FF001EE0087820F00F0095 +:1060C000401C20F0F000103016E0087820F00F009F +:1060D000401C20F0F00020300EE0087820F00F0087 +:1060E000401C20F0F000303006E0087820F00F006F +:1060F000401C20F0F000403008700020704707205E +:1061000070472DE9F041804688B00D4600270846CB +:10611000FBF7ECF8A8B94046FDF794F9040003D06A +:106120002078222815D104E043F2020008B0BDE82F +:10613000F08145B9A07F410603D500F00300022895 +:1061400001D01020F2E7A07FC10601D4010702D5DB +:106150000DB10820EAE7E17F090701D50D20E5E749 +:1061600000F0030002280DD165B12846FEF75EFF5E +:106170000700DBD1FBF736FB20B9E878800701D5B3 +:106180000620D3E7A07F00F00300022808D00020FB +:1061900080F0010140460DF089FE060002D00FE0BC +:1061A0000120F5E7A07F00F0030002280ED00020B8 +:1061B00080F00101002240460DF06FFE060007D07E +:1061C000A07F00F00300022804D009E00120EFE7DF +:1061D0000420ABE725B12A4631462046FEF74AFFA8 +:1061E0006946304600F017FD009800B9FFDF0099BE +:1061F000022006F1E0024870C1F824804A610022C2 +:106200000A81A27F02F00302022A1CD00120087139 +:10621000287800F00102087E62F3010008762A78EF +:10622000520862F3820008762A78920862F3C3006B +:1062300008762A78D20862F30410087624212046D2 +:10624000FCF768FF33E035B30871301D88613078A2 +:10625000400908777078C0F340004877287800F04C +:106260000102887F62F301008877A27FD20962F37E +:1062700082008877E27F62F3C3008877727862F3E6 +:1062800004108877A878C87701F1210228462031C8 +:10629000FEF711FF03E00320087105200876252191 +:1062A0002046FCF737FFA07F20F04000A07701A92F +:1062B00000980FF0F4FA022801D000B1FFDF384651 +:1062C00034E72DE9FF4F8DB09A4693460D460027DF +:1062D0000D98FDF7B7F8060006D03078262806D0CE +:1062E000082011B0BDE8F08F43F20200F9E7B07F5B +:1062F00000F00309B9F1020F11D04DB95846FEF76D +:1063000095FE0028EDD1B07F00F00300022806D0F2 +:10631000BBF1000F11D0FBF765FA20B10DE0BBF126 +:10632000000F50D109E006200DF068FD28B19BF860 +:106330000300800701D50620D3E7B07F00F00300FB +:10634000022809D05FF0000080F001010D980DF0E7 +:10635000ADFD040003D101E00120F5E7FFDF852D4D +:1063600027D007DCEDB1812D1DD0822D1DD0832DCE +:1063700008D11CE0862D1ED0882D1ED0892D1ED060 +:106380008A2D1ED00F2020710F281CD003F02EF96B +:10639000D8B101208DF81400201D06902079B0B1ED +:1063A00056E10020EFE70120EDE70220EBE70320B4 +:1063B000E9E70520E7E70620E5E70820E3E709200D +:1063C000E1E70A20DFE707208BE7112089E7B9F131 +:1063D000020F03D0A56F03D1A06F02E0656FFAE74B +:1063E000606F804631D04FF0010001904FF0020005 +:1063F00000905A4621463046FEF73CFE02E000007F +:10640000300200209BF8000000F00101A87861F341 +:106410000100A870B17FC90961F38200A870F17F03 +:1064200061F3C300A870617861F30410A87020784C +:10643000400928706078C0F3400068709BF8020043 +:10644000E87000206871287103E0022001900120AB +:106450000090A87898F80210C0F3C000C1F3C00102 +:10646000084003902CD05046FAF7F3FEC0BBDAF890 +:106470000C00FAF7EEFE98BBDAF81C00FAF7E9FE1A +:1064800070BBDAF80C00A060DAF81C00E0606078FD +:1064900098F8012042EA500161F34100607098F8D9 +:1064A0000210C0B200EA111161F300006070002018 +:1064B0002077009906F11700022907D0012106E094 +:1064C000607898F8012002EA5001E5E7002104EB2A +:1064D000810148610199701C022902D0012101E06B +:1064E00028E0002104EB81014861A87800F0030056 +:1064F000012857D198F8020000F00300012851D17B +:10650000B9F1020F04D02A1D691D5846FEF7D3FDCC +:10651000287998F8041008408DF82C00697998F8CB +:10652000052011408DF8301008433BD05046FAF753 +:1065300090FE08B11020D4E60AF110018B46B9F1A3 +:10654000020F17D00846002104F18C03CDE90003A7 +:1065500004F5AE7202920BAB2046039AFEF7F4FDEF +:106560000028E8D1B9F1020F08D0504608D14FF009 +:10657000010107E050464FF00101E5E75846F5E715 +:106580004FF0000104F1A403CDE9000304F5B0725B +:10659000029281F001010CAB2046039AFEF7D4FD74 +:1065A0000028C8D16078800733D4A87898F8021002 +:1065B000C0F38000C1F3800108432AD0297898F8FD +:1065C0000000F94AB9F1020F06D032F81120430059 +:1065D000DA4002F003070AE032F810204B00DA40FC +:1065E00012F0030705D0012F0AD0022F0AD0032F83 +:1065F00006D0039A6AB1012906D0042904D008E024 +:106600000227F6E70127F4E7012801D0042800D18A +:106610000427B07F40F08000B077F17F039860F3EB +:106620000001F1776078800705D50320A0710398F9 +:1066300070B9002029E00220022F18D0012F18D0B5 +:10664000042F2AD00020A071B07F20F08000B07706 +:1066500025213046FCF75EFD05A904F1E0000FF0AE +:1066600003F910B1022800D0FFDF002039E6A07145 +:10667000DFE7A0710D22002104F1200007F007FFE1 +:10668000207840F00200207001208DF8100004AA4C +:1066900031460D9800F098FADAE70120A071D7E7AB +:1066A0002DE9F04387B09046894604460025FCF763 +:1066B000C9FE060006D03078272806D0082007B08B +:1066C000BDE8F08343F20200F9E7B07F00F0030079 +:1066D000022809D05FF0000080F0010120460DF093 +:1066E000E5FB040003D101E00120F5E7FFDFA77916 +:1066F0005FEA090005D0012821D0B9F1020F26D1A7 +:1067000010E0B8F1000F22D1012F05D0022F05D0E3 +:10671000032F05D0FFDF2EE00C252CE001252AE019 +:10672000022528E04046FAF794FDB0B9032F0ED1B8 +:106730001022414604F11D0007F07FFE1BE0012FEF +:1067400002D0022F03D104E0B8F1000F13D00720CC +:10675000B5E74046FAF77DFD08B11020AFE71022FB +:10676000002104F11D0007F092FE0621404607F0CB +:10677000E1FEC4F81D002078252140F002002070C1 +:106780003046FCF7C7FC2078C10713D020F0010089 +:10679000207002208DF8000004F11D0002908DF899 +:1067A00004506946C3300FF05FF8022803D010B1DF +:1067B000FFDF00E02577002081E730B587B00D4688 +:1067C0000446FCF73FFE98B1807F00F003000228EA +:1067D00011D0002080F0010120460DF067FB04007D +:1067E0000ED02846FAF735FD38B1102007B030BD7D +:1067F00043F20200FAE70120ECE72078400701D4D9 +:106800000820F3E7294604F13D002022054607F061 +:1068100014FE207840F01000207001070FD520F002 +:106820000800207007208DF80000694604F1E000A0 +:1068300001950FF019F8022801D000B1FFDF002008 +:10684000D4E770B50D460646FCF7FCFD18B101789B +:10685000272921D102E043F2020070BD807F00F0C1 +:106860000300022808D0002080F0010130460DF01E +:106870001DFB040003D101E00120F5E7FFDFA07953 +:10688000022809D16078C00706D02A462146304642 +:10689000FEF7EBFC10B10FE0082070BDB4F860000B +:1068A0000E280BD204F1620102231022081F0DF002 +:1068B00084F9012101704570002070BD112070BD68 +:1068C00070B5064614460D460846FAF7C2FC18B9DC +:1068D0002046FAF7E4FC08B1102070BDA6F57F4011 +:1068E000FF380ED03046FCF7ADFD38B14178224676 +:1068F0004B08811C1846FCF774FD07E043F20200C8 +:1069000070BD2046FDF7A5FD0028F9D11021E01D3E +:1069100010F0EDFDE21D294604F1170000F08BF99F +:10692000002070BD2DE9F04104468AB01546884626 +:1069300000270846FAF7DAFC18B92846FAF7D6FC19 +:1069400018B110200AB0BDE8F0812046FCF77AFDAE +:10695000060003D0307827281BD102E043F2020062 +:10696000F0E7B07F00F00300022809D05FF00000DC +:1069700080F0010120460DF099FA040003D101E0F6 +:106980000120F5E7FFDF2078400702D56078800717 +:1069900001D40820D6E7B07F00F00300022805D01C +:1069A000A06F05D1A16F04E01C610200606FF8E7E1 +:1069B000616F407800B19DB1487810B1B8F1000F17 +:1069C0000ED0ADB1EA1D06A8E16800F034F910223E +:1069D00006A905F1170007F003FD18B1042707E029 +:1069E0000720AFE71022E91D04F12D0007F025FD77 +:1069F000B8F1000F06D0102208F1070104F11D00C4 +:106A000007F01BFD2078252140F002002070304661 +:106A1000FCF780FB2078C10715D020F00100207022 +:106A200002208DF8000004F11D0002901030039048 +:106A30008DF804706946B3300EF016FF022803D0BB +:106A400010B1FFDF00E0277700207BE7F8B515469F +:106A50000E460746FCF7F6FC040004D020782228F6 +:106A600004D00820F8BD43F20200F8BDA07F00F07A +:106A70000300022802D043F20500F8BD3046FAF7C1 +:106A8000E8FB18B92846FAF7E4FB08B11020F8BD76 +:106A900000953288B31C21463846FEF709FC1128C0 +:106AA00015D00028F3D1297C4A08A17F62F3C711D1 +:106AB000A177297CE27F61F30002E277297C8908D3 +:106AC00084F82010A17F21F04001A177F8BDA17FBB +:106AD0000907FBD4D6F80200C4F83600D6F8060041 +:106AE000C4F83A003088A0861022294604F1240018 +:106AF00007F0A3FC287C4108E07F61F34100E077C8 +:106B0000297C61F38200E077287C800884F82100EA +:106B1000A07F40F00800A0770020D3E770B50D46B5 +:106B200006460BB1072070BDFCF78CFC040007D0B3 +:106B30002078222802D3A07F800604D4082070BDCC +:106B400043F2020070BDADB1294630460CF076F834 +:106B500002F069FB297C4A08A17F62F3C711A17783 +:106B6000297CE27F61F30002E277297C890884F8BE +:106B7000201004E030460CF084F802F054FBA17FB2 +:106B800021F02001A17770BD70B50D46FCF75AFCCD +:106B9000040005D02846FAF782FB20B1102070BD12 +:106BA00043F2020070BD29462046FEF72FFB00206D +:106BB00070BD04E010F8012B0AB100207047491E97 +:106BC00089B2F7D20120704770B51546064602F02B +:106BD0000DFD040000D1FFDF207820F00F00801CA5 +:106BE00020F0F0002030207066802868A060BDE8AA +:106BF000704002F0FEBC10B5134C94F83000002831 +:106C000008D104F12001A1F110000EF06FFE012067 +:106C100084F8300010BD10B190F8B9202AB10A48AC +:106C200090F8350018B1002003E0B83001E00648C4 +:106C300034300860704708B50023009313460A46B5 +:106C40000DF031FB08BD00003002002018B1817842 +:106C5000012938D101E010207047018842F6011265 +:106C6000881A914231D018DC42F60102A1EB0200F1 +:106C700091422AD00CDC41B3B1F5C05F25D06FF44E +:106C8000C050081821D0A0F57060FF381BD11CE05F +:106C900001281AD002280AD117E0B0F5807F14D05D +:106CA00008DC012811D002280FD003280DD0FF28BE +:106CB00009D10AE0B0F5817F07D0A0F580700338D4 +:106CC00003D0012801D0002070470F2070470A2808 +:106CD0001FD008DC0A2818D2DFE800F0191B1F1F9C +:106CE000171F231D1F21102815D008DC0B2812D0D8 +:106CF0000C2810D00D2816D00F2806D10DE0112831 +:106D00000BD084280BD087280FD003207047002099 +:106D1000704705207047072070470F2070470420F8 +:106D20007047062070470C20704743F202007047FE +:106D300038B50C46050041D06946FFF797F90028A1 +:106D400019D19DF80010607861F302006070694607 +:106D5000681CFFF78BF900280DD19DF800106078B2 +:106D600061F3C5006070A978C1F34101012903D026 +:106D7000022905D0072038BD217821F0200102E04A +:106D8000217841F020012170410704D0A978C90879 +:106D900061F386106070607810F0380F07D0A97822 +:106DA000090961F3C710607010F0380F02D16078E4 +:106DB000400603D5207840F040002070002038BD08 +:106DC00070B504460020088015466068FFF7B0FFE4 +:106DD000002816D12089A189884211D8606880785E +:106DE000C0070AD0B1F5007F0AD840F20120B1FBFC +:106DF000F0F200FB1210288007E0B1F5FF7F01D907 +:106E00000C2070BD01F201212980002070BD10B559 +:106E10000478137864F3000313700478640864F34F +:106E2000410313700478A40864F382031370047898 +:106E3000E40864F3C30313700478240964F30413AF +:106E400013700478640964F34513137000788009A3 +:106E500060F38613137031B10878C10701D1800740 +:106E600001D5012000E0002060F3C713137010BDAE +:106E70004278530702D002F0070306E012F0380F01 +:106E800002D0C2F3C20300E001234A7863F3020296 +:106E90004A70407810F0380F02D0C0F3C20005E00D +:106EA000430702D000F0070000E0012060F3C502B4 +:106EB0004A7070472DE9F04F95B00D00824613D00F +:106EC00012220021284607F0E2FA4FF6FF7B05AABE +:106ED0000121584607F0A3F80024264637464FF410 +:106EE00020586FF4205972E0102015B0BDE8F08FE3 +:106EF0009DF81E0001280AD1BDF81C1041450BD099 +:106F000011EB09000AD001280CD002280CD0042C67 +:106F10000ED0052C0FD10DE0012400E00224BDF8B5 +:106F20001A6008E0032406E00424BDF81A7002E0A9 +:106F3000052400E00624BDF81A10514547D12C74F1 +:106F4000BEB34FF0000810AA4FF0070ACDE9028245 +:106F5000CDE900A80DF13C091023CDF81090424670 +:106F60003146584607F02BF908BBBDF83C002A46CD +:106F7000C0B210A90EF045FDC8B9AE81CFB1CDE9C0 +:106F800000A80DF1080C0AAE40468CE8410213231C +:106F900000223946584607F012F940B9BDF83C00C6 +:106FA000F11CC01EC0B22A1D0EF02BFD10B1032033 +:106FB0009BE70AE0BDF82900E881062C05D19DF881 +:106FC0001E00A872BDF81C00288100208DE705A8CE +:106FD00007F031F800288BD0FFF779FE85E72DE91F +:106FE000F0471C46DDE90978DDF8209015460E00D3 +:106FF000824600D1FFDF0CB1208818B1D5B1112035 +:10700000BDE8F087022D01D0012100E0002106F14A +:10701000140005F0CDFEA8F8000002463B462946C4 +:10702000504603F092F9C9F8000008B9A41C3C606E +:107030000020E5E71320E3E7F0B41446DDE904524D +:107040008DB1002314B1022C09D101E0012306E027 +:107050000D7CEE0703D025F0010501230D742146B8 +:10706000F0BC04F050BA1A80F0BC70472DE9FE4F16 +:1070700091461A881C468A468046FAB102AB4946B8 +:1070800003F063F9050019D04046A61C27880DF0CF +:1070900050F83246072629463B4600960CF05FFC26 +:1070A00020882346CDE900504A4651464046FFF726 +:1070B000C3FF002020800120BDE8FE8F0020FBE7F9 +:1070C0002DE9F04786B082460EA8904690E8B000C1 +:1070D000894604AA05A903A88DE807001E462A468A +:1070E00021465046FFF77BFF039901B1012139701A +:1070F000002818D1FA4904F1140204AB086003987F +:1071000005998DE8070042464946504606F003FAC5 +:10711000A8B1092811D2DFE800F005080510100A0F +:107120000C0C0E00002006B06AE71120FBE70720D8 +:10713000F9E70820F7E70D20F5E70320F3E7BDF8AE +:1071400010100398CDE9000133462A4621465046E7 +:10715000FFF772FFE6E72DE9F04389B01646DDE957 +:1071600010870D4681461C461422002103A807F013 +:107170008EF9012002218DF810108DF80C008DF889 +:107180001170ADF8146064B1A278D20709D08DF8FF +:107190001600E088ADF81A00A088ADF81800A068C5 +:1071A000079008A80095CDE90110424603A948467A +:1071B0006B68FFF785FF09B0BDE8F083F0B58BB0D1 +:1071C00000240646069407940727089405A8099406 +:1071D000019400970294CDE903400D461023224606 +:1071E000304606F0ECFF78B90AA806A9019400978A +:1071F0000294CDE90310BDF8143000222946304630 +:1072000006F07BFD002801D0FFF761FD0BB0F0BD5B +:1072100006F00CBC2DE9FC410C468046002602F02D +:10722000E5F9054620780D287ED2DFE800F0BC079E +:1072300013B325BD49496383AF959B00A8480068F7 +:1072400020B1417841F010014170ADE0404602F0BC +:10725000FDF9A9E0042140460CF028FE070000D10A +:10726000FFDF07F11401404605F037FDA5BB1321F0 +:107270004046FDF7CFFB97E0042140460CF016FE98 +:10728000070000D1FFDFE088ADF800000020B881E2 +:107290009DF80000010704D5C00602D5A088B8817A +:1072A00005E09DF8010040067ED5A088F88105B96B +:1072B000FFDF22462946404601F0ACFC022673E07F +:1072C000E188ADF800109DF8011009060FD50728D8 +:1072D00003D006280AD00AE024E0042140460CF03E +:1072E000E5FD060000D1FFDFA088F0810226CDB9C0 +:1072F000FFDF17E0042140460CF0D8FD070000D165 +:10730000FFDF07F1140006F0C8FB90F0010F02D177 +:10731000E079000648D5387C022640F00200387437 +:1073200005B9FFDF224600E03DE02946404601F076 +:1073300071FC39E0042140460CF0B8FD017C002DC1 +:1073400001F00206C1F340016171017C21F00201EC +:107350000174E7D1FFDFE5E702260121404602F094 +:10736000A7F921E0042140460CF0A0FD0546606825 +:1073700000902089ADF8040001226946404602F0E1 +:10738000B8F9287C20F0020028740DE0002DC9D146 +:10739000FFDFC7E7022600214046FBF799F8002DE2 +:1073A000C0D1FFDFBEE7FFDF3046BDE8FC813EB560 +:1073B0000C0009D001466B4601AA002006F084FFAC +:1073C00020B1FFF784FC3EBD10203EBD0020208090 +:1073D000A0709DF8050002A900F00700FEF762FE0C +:1073E00050B99DF8080020709DF8050002A9C0F36F +:1073F000C200FEF757FE08B103203EBD9DF808000D +:1074000060709DF80500C109A07861F30410A070B8 +:107410009DF80510890961F3C300A0709DF8041060 +:10742000890601D5022100E0012161F342009DF8A7 +:10743000001061F30000A07000203EBD70B514463E +:1074400006460D4651EA040005D075B10846F9F725 +:1074500044FF78B901E0072070BD2946304606F0A8 +:107460009AFF10B1BDE8704031E454B12046F9F7FD +:1074700034FF08B1102070BD21463046BDE8704091 +:1074800095E7002070BD2DE9FC5F0C46904605464F +:10749000002701780822007A3E46B2EB111F7DD109 +:1074A00004F10A0100910A31821E4FF0020A04F130 +:1074B000080B0191092A72D2DFE802F0EDE005F530 +:1074C00028287BAACE00688804210CF0EFFC060077 +:1074D00000D1FFDFB08928B152270726C3E00000A2 +:1074E0009402002051271026002C7DD06888A080AF +:1074F0000120A071A88900220099FFF79FFF0028B2 +:1075000073D1A8892081288AE081D1E0B5F8129052 +:10751000072824D1E87B000621D5512709F1140062 +:1075200086B2002CE1D0A88900220099FFF786FFDF +:1075300000285AD16888A08084F806A0A8892081F4 +:107540000120A073288A2082A4F81290A88A0090B3 +:1075500068884B46A969019A01F038FBA8E05027DA +:1075600009F1120086B2002C3ED0A88900225946AB +:10757000FFF764FF002838D16888A080A889E080E0 +:10758000287A072813D002202073288AE081E87B1C +:10759000C0096073A4F81090A88A01E085E082E039 +:1075A000009068884B4604F11202A969D4E70120D3 +:1075B000EAE7B5F81290512709F1140086B2002CC1 +:1075C00066D0688804210CF071FC83466888A0802E +:1075D000A88900220099FFF731FF00286ED184F8B6 +:1075E00006A0A889208101E052E067E00420A07392 +:1075F000288A2082A4F81290A88A009068884B46B6 +:10760000A969019A01F0E2FAA989ABF80E104FE0DE +:107610006888FBF717FF0746688804210CF046FCD2 +:10762000064607B9FFDF06B9FFDF687BC00702D057 +:107630005127142601E0502712264CB36888A080F9 +:10764000502F06D084F806A0287B594601F0CEFAC8 +:107650002EE0287BA11DF9E7FE49A88949898142CE +:1076600005D1542706269CB16888A08020E05327C6 +:107670000BE06888A080A889E08019E06888042170 +:107680000CF014FC00B9FFDF55270826002CF0D1C0 +:10769000A8F8006011E056270726002CF8D068886B +:1076A000A080002013E0FFDF02E0012808D0FFDF08 +:1076B000A8F800600CB1278066800020BDE8FC9F20 +:1076C00057270726002CE3D06888A080687AA0712D +:1076D000EEE7401D20F0030009B14143091D01EB15 +:1076E0004000704713B5DB4A00201071009848B184 +:1076F000002468460CF0F7F9002C02D1D64A009914 +:1077000011601CBD01240020F4E770B50D4614463D +:10771000064686B05C220021284606F0B8FE04B971 +:10772000FFDFA0786874A2782188284601F089FAE2 +:107730000020A881E881228805F11401304605F077 +:10774000B0FA6A460121304606F069FC1AE000BF33 +:107750009DF80300000715D5BDF806103046FFF769 +:107760002DFD9DF80300BDF8061040F010008DF8C7 +:107770000300BDF80300ADF81400FF233046059A5E +:1077800006F0D1FD684606F056FC0028E0D006B0B1 +:1077900070BD10B50C4601F1140005F0BAFA0146AF +:1077A000627C2046BDE8104001F080BA30B5044646 +:1077B000A84891B04FF6FF75C18905AA284606F082 +:1077C0002EFC30E09DF81E00A0422AD001282AD1CC +:1077D000BDF81C00B0F5205F03D042F601018842DD +:1077E00021D1002002AB0AAA0CA9019083E807006E +:1077F00007200090BDF81A1010230022284606F03A +:10780000DEFC38B9BDF828000BAAC0B20CA90EF0F6 +:10781000F8F810B1032011B030BD9DF82E00A04241 +:1078200001D10020F7E705A806F005FC0028C9D023 +:107830000520F0E770B5054604210CF037FB040085 +:1078400000D1FFDF04F114010C46284605F045FA8B +:1078500021462846BDE8704005F046BA70B58AB0AA +:107860000C460646FBF7EEFD050014D028782228CA +:1078700027D30CB1A08890B101208DF80C00032013 +:107880008DF8100000208DF8110054B1A088ADF8DB +:107890001800206807E043F202000AB070BD09201A +:1078A000FBE7ADF818000590042130460CF0FEFA15 +:1078B000040000D1FFDF04F1140005F040FA0007D6 +:1078C00001D40820E9E701F091FE60B108A8022187 +:1078D0000094CDE9011095F8232003A93046636890 +:1078E000FFF7EEFBD9E71120D7E72DE9F04FB2F80B +:1078F00002A0834689B0154689465046FBF7A2FD93 +:107900000746042150460CF0D1FA0026044605969D +:107910004FF002080696ADF81C6007B9FFDF04B906 +:10792000FFDF4146504603F055FF50B907AA06A9AC +:1079300005A88DE807004246214650466368FFF7D8 +:107940004EFB444807AB0660DDE9051204F1140064 +:10795000CDF80090CDE90320CDE9013197F823203F +:10796000594650466B6805F033FA06000AD0022EDD +:1079700004D0032E14D0042E00D0FFDF09B030460F +:10798000BDE8F08FBDF81C000028F7D00599CDE9BF +:1079900000104246214650466368FFF74DFBEDE775 +:1079A000687840F008006870E8E710B50C46FFF70B +:1079B000BFF900280BD1607800F00701012905D13B +:1079C00010F0380F02D02078810601D5072010BDB5 +:1079D00040F0C8002070002010BD2DE9F04F99B094 +:1079E00004464FF000081B48ADF81C80ADF820801D +:1079F000ADF82480A0F80880ADF81480ADF81880A8 +:107A0000ADF82880ADF82C80007916460D46474623 +:107A1000012808D0022806D0032804D0042802D068 +:107A2000082019B0ACE72046F9F713FCF0BB284654 +:107A3000F9F70FFCD0BB6068F9F758FCB0BB606881 +:107A400068B160892189884202D8B1F5007F05D9E3 +:107A50000C20E6E7940200201800002080460EAAC1 +:107A600006A92846FFF7ACF90028DAD168688078C3 +:107A7000C0F34100022808D19DF8190010F0380F1A +:107A800003D02869F9F729FC80B905A92069FFF717 +:107A90004FF90028C5D1206950B1607880079DF862 +:107AA000150000F0380002D5F0B301E011E0D8BBBA +:107AB0009DF8140080060ED59DF8150010F0380FC3 +:107AC00003D06068F9F709FC18B96068F9F70EFC93 +:107AD00008B11020A5E70BA906A8FFF7C9F99DF882 +:107AE0002D000BA920F00700401C8DF82D006069C7 +:107AF000FFF75BFF002894D10AA9A069FFF718F9E6 +:107B000000288ED19DF8280080062BD4A06940B1B2 +:107B10009DF8290000F00701012923D110F0380F4A +:107B200020D0E06828B100E01CE00078D0B11C282B +:107B300018D20FAA611C2046FFF769F901213846C7 +:107B400061F30F2082468DF85210B94642F60300C9 +:107B50000F46ADF850000DF13F0218A928680DF04E +:107B600052FF08B107205CE79DF8600015A9CDF829 +:107B70000090C01CCDE9019100F0FF0B00230BF237 +:107B80000122514614A806F075F9E8BBBDF854006F +:107B90000C90FB482A8929690092CDE901106B8974 +:107BA000BDF838202868069906F064F9010077D1FD +:107BB00020784FF0020AC10601D4800616D58DF850 +:107BC000527042F60210ADF85000CDF80C9008A9A2 +:107BD00003AACDF800A0CDE90121002340F2032241 +:107BE00014A80B9906F046F9010059D1E4484D4616 +:107BF00008380089ADF83D000FA8CDE90290CDF816 +:107C00000490CDF8109000E00CE04FF007095B46BF +:107C10000022CDF80090BDF854104FF6FF7006F02A +:107C20006CF810B1FFF753F8FBE69DF83C00000636 +:107C300024D52946012060F30F218DF852704FF4AE +:107C400024500395ADF8500062789DF80C00002395 +:107C500062F300008DF80C006278CDF800A05208A5 +:107C600062F341008DF80C0003AACDE9012540F232 +:107C7000032214A806F0FEF8010011D1606880B359 +:107C80002069A0B905A906A8FFF7F2F86078800777 +:107C900007D49DF8150020F038008DF8150006E097 +:107CA00077E09DF8140040F040008DF814008DF846 +:107CB000527042F60110ADF85000208940F20121C7 +:107CC000B0FBF1F201FB1202606809ABCDF8008055 +:107CD000CDE90103002314A8059906F0CBF80100B3 +:107CE00057D12078C00728D00395A06950B90AA9B8 +:107CF00006A8FFF7BDF89DF8290020F00700401CFA +:107D00008DF829009DF8280007A940F040008DF863 +:107D100028008DF8527042F60310ADF8500003AA07 +:107D2000CDF800A0CDE90121002340F2032214A8E0 +:107D30000A9906F09FF801002BD1E06868B3294644 +:107D4000012060F30F218DF8527042F60410ADF857 +:107D50005000E068002302788DF8582040788DF8B4 +:107D60005900E06816AA4088ADF85A00E06800792A +:107D70008DF85C00E068C088ADF85D00CDF800903B +:107D8000CDE901254FF4027214A806F073F8010042 +:107D900003D00C9800F0B6FF43E679480321083879 +:107DA000017156B100893080BDF824007080BDF8A3 +:107DB0002000B080BDF81C00F080002031E670B5D6 +:107DC00001258AB016460B46012802D0022816D19A +:107DD00004E08DF80E504FF4205003E08DF80E5063 +:107DE00042F60100ADF80C005BB10024601C60F3AA +:107DF0000F2404AA08A918460DF005FE18B10720A3 +:107E00004BE5102049E504A99DF820205C48CDE908 +:107E10000021801E02900023214603A802F20122C5 +:107E200006F028F810B1FEF752FF36E5544808383E +:107E30000EB1C1883180057100202EE5F0B593B0F8 +:107E4000044601268DF83E6041F601000F46ADF86C +:107E50003C0011AA0FA93046FFF7B1FF002837D127 +:107E60002000474C4FF00005A4F1080432D01C223A +:107E7000002102A806F00BFB9DF808008DF83E607B +:107E800040F020008DF8080042F60520ADF83C00D7 +:107E900004200797ADF82C00ADF8300039480A905F +:107EA0000EA80D900E950FA80990ADF82E506A46B9 +:107EB00009A902A8FFF791FD002809D1BDF800002B +:107EC0006081BDF80400A081401CE0812571002084 +:107ED00013B0F0BD6581A581BDF84400F4E72DE93C +:107EE000F74F2749A0B00024083917940A79A14612 +:107EF000012A04D0022A02D0082023B040E5CA8813 +:107F0000824201D00620F8E721988A46824201D1B8 +:107F10000720F2E70120214660F30F21ADF8480069 +:107F20004FF6FF788DF86E000691ADF84A8042F664 +:107F3000020B8DF872401CA9ADF86CB0ADF8704022 +:107F40001391ADF8508012A806F0A4F800252E4633 +:107F50002F460DAB072212A9404606F09EF898B1B5 +:107F60000A2861D1B5B3AEB3ADF86450ADF8666020 +:107F70009DF85E008DF8144019AC012868D06FE0C0 +:107F80009C020020266102009DF83A001FB30128E0 +:107F900059D1BDF8381059451FD118A809A9019425 +:107FA0000294CDE9031007200090BDF8361010238D +:107FB0000022404606F003F9B0BBBDF8600004287B +:107FC00001D006284AD1BDF82410219881423AD127 +:107FD0000F2092E73AE0012835D1BDF83800B0F51E +:107FE000205F03D042F6010188422CD1BAF8060086 +:107FF000BDF83610884201D1012700E0002705B105 +:108000009EB1219881421ED118A809AA0194029418 +:10801000CDE90320072000900D46102300224046A2 +:1080200006F0CDF800B902E02DE04E460BE0BDF8B9 +:108030006000022801D0102810D1C0B217AA09A9E7 +:108040000DF0DFFC50B9BDF8369082E7052054E70B +:1080500005A917A8221D0DF0D6FC08B103204CE796 +:108060009DF814000023001DC2B28DF81420229840 +:108070000092CDE901401BA8069905F0FBFE10B95E +:1080800002228AF80420FEF722FE36E710B50B46DE +:10809000401E88B084B205AA00211846FEF7B7FE3C +:1080A00000200DF1080C06AA05A901908CE8070034 +:1080B000072000900123002221464FF6FF7005F0B3 +:1080C0001CFE0446BDF81800012800D0FFDF204642 +:1080D000FEF7FDFD08B010BDF0B5FF4F044687B0B8 +:1080E00038790E46032804D0042802D0082007B0AF +:1080F000F0BD04AA03A92046FEF762FE0500F6D1F2 +:1081000060688078C0F3410002280AD19DF80D0014 +:1081100010F0380F05D02069F9F7DFF808B110200A +:10812000E5E7208905AA21698DE807006389BDF884 +:1081300010202068039905F09DFE10B1FEF7C7FDE1 +:10814000D5E716B1BDF814003080042038712846F8 +:10815000CDE7F8B50C0006460BD001464FF6FF758B +:1081600000236A46284606F0AFF820B1FEF7AFFDBF +:10817000F8BD1020F8BD69462046FEF7D9FD00285D +:10818000F8D1A078314600F001032846009A06F0A5 +:10819000CAF8EBE730B587B0144600220DF1080CA1 +:1081A00005AD01928CE82C00072200920A46014698 +:1081B00023884FF6FF7005F0A0FDBDF81410218054 +:1081C000FEF785FD07B030BD70B50D4604210BF0FC +:1081D0006DFE040000D1FFDF294604F11400BDE864 +:1081E000704004F0A5BD70B50D4604210BF05EFE95 +:1081F000040000D1FFDF294604F11400BDE87040FF +:1082000004F0B9BD70B50D4604210BF04FFE04001B +:1082100000D1FFDF294604F11400BDE8704004F0EE +:10822000D1BD70B5054604210BF040FE040000D11D +:10823000FFDF214628462368BDE870400122FEF793 +:1082400015BF70B5064604210BF030FE040000D1C6 +:10825000FFDF04F1140004F05CFD401D20F0030575 +:1082600011E0011D00880022431821463046FEF728 +:10827000FDFE00280BD0607CABB2684382B2A068E0 +:10828000011D0BF0D0FCA06841880029E9D170BD28 +:1082900070B5054604210BF009FE040000D1FFDF94 +:1082A000214628466368BDE870400222FEF7DEBE24 +:1082B00070B50E46054601F099F9040000D1FFDFC4 +:1082C0000120207266726580207820F00F00001D6A +:1082D00020F0F00040302070BDE8704001F089B916 +:1082E00010B50446012900D0FFDF2046BDE810404C +:1082F0000121FAF7EDB82DE9F04F97B04FF0000AE1 +:108300000C008346ADF814A0D04619D0E06830B117 +:10831000A068A8B10188ADF81410A0F800A05846D4 +:10832000FBF790F8070043F2020961D03878222861 +:108330005CD3042158460BF0B9FD050005D103E0DC +:10834000102017B0BDE8F08FFFDF05F1140004F036 +:10835000E0FC401D20F00306A078012803D002288D +:1083600001D00720EDE7218807AA584605F057FEFF +:1083700030BB07A805F05FFE10BB07A805F05BFE49 +:1083800048B99DF82600012805D1BDF82400A0F5C4 +:108390002451023902D04FF45050D2E7E068B0B116 +:1083A000CDE902A00720009005AACDF804A0049210 +:1083B000A2882188BDF81430584605F09EFC10B103 +:1083C000FEF785FCBDE7A168BDF8140008809DF8A4 +:1083D0001F00C00602D543F20140B2E70B9838B146 +:1083E000A1780078012905D080071AD40820A8E7D1 +:1083F0004846A6E7C007F9D002208DF83C00A868DF +:108400004FF00009A0B1697C4288714391420FD9B5 +:108410008AB2B3B2011D0BF0BCFB8046A0F800A0ED +:1084200006E003208DF83C00D5F800804FF00109EC +:108430009DF8200010F0380F00D1FFDF9DF82000DC +:108440002649C0F3C200084497F8231010F8010C25 +:10845000884201D90F2074E72088ADF8400014A9A4 +:108460000095CDE90191434607220FA95846FEF732 +:1084700027FE002891D19DF8500050B9A07801281E +:1084800007D1687CB3B2704382B2A868011D0BF0BB +:1084900094FB002055E770B5064615460C46084685 +:1084A000FEF7D4FB002805D12A4621463046BDE818 +:1084B000704084E470BD12E570B51E4614460D0090 +:1084C0000ED06CB1616859B160B10349C98881426D +:1084D00008D0072070BD000094020020296102002E +:1084E0001020F7E72068FEF7B1FB0028F2D13246F2 +:1084F00021462846BDE87040FFF76FBA70B51546B3 +:108500000C0006D038B1FE490989814203D007200A +:10851000E0E71020DEE72068FEF798FB0028D9D1BD +:1085200029462046BDE87040D6E570B5064686B0BF +:108530000D4614461046F8F7B2FED0BB6068F8F757 +:10854000D5FEB0BBA6F57F40FF3803D03046FAF722 +:1085500079FF80B128466946FEF7ACFC00280CD1B3 +:108560009DF810100F2008293DD2DFE801F0080621 +:108570000606060A0A0843F2020006B0AAE703202C +:10858000FBE79DF80210012908D1BDF80010B1F5F4 +:10859000C05FF2D06FF4C052D142EED09DF8061009 +:1085A00001290DD1BDF80410A1F52851062907D2E3 +:1085B00000E029E0DFE801F0030304030303DCE744 +:1085C0009DF80A1001290FD1BDF80810B1F5245FFC +:1085D000D3D0A1F60211B1F50051CED00129CCD0F3 +:1085E000022901D1C9E7FFDF606878B9002305AA35 +:1085F0002946304605F068FE10B1FEF768FBBCE77F +:108600009DF81400800601D41020B6E76188224648 +:1086100028466368FFF7BEFDAFE72DE9F0438146CA +:1086200087B0884614461046F8F739FE18B1102076 +:1086300007B0BDE8F083002306AA4146484605F08E +:1086400043FE10B1FEF743FBF2E79DF81800C006A9 +:1086500002D543F20140EBE70025072705A8019565 +:1086600000970295CDE9035062884FF6FF734146AB +:10867000484605F0A4FD060013D16068F8F70FFE28 +:1086800060B960680195CDE9025000970495238890 +:1086900062884146484605F092FD0646BDF8140042 +:1086A00020803046CEE739B1954B0A889B899A42A3 +:1086B00002D843F2030070471DE610B586B0904C17 +:1086C0000423ADF81430638943B1A4898C4201D2EC +:1086D000914205D943F2030006B010BD0620FBE726 +:1086E000ADF81010002100910191ADF80030022189 +:1086F0008DF8021005A9029104A90391ADF812208A +:108700006946FFF7F8FDE7E72DE9FC4781460D468E +:108710000846F8F79EFD88BB4846FAF793FE5FEAE5 +:1087200000080AD098F80000222829D304214846DE +:108730000BF0BCFB070005D103E043F20200BDE8EB +:10874000FC87FFDF07F1140004F0F9FA06462878E9 +:10875000012803D0022804D00720F0E7B0070FD586 +:1087600002E016F01C0F0BD0A8792C1DC00709D011 +:10877000E08838B1A068F8F76CFD18B11020DEE78A +:108780000820DCE721882A780720B1F5847F35D0DE +:108790001EDC40F20315A1F20313A94226D00EDC21 +:1087A000B1F5807FCBD003DCF9B1012926D1C6E732 +:1087B000A1F58073013BC2D0012B1FD113E0012B27 +:1087C000BDD0022B1AD0032BB9D0042B16D112E046 +:1087D000A1F20912082A11D2DFE802F00B040410FA +:1087E00010101004ABE7022AA9D007E0012AA6D096 +:1087F00004E0320700E0F206002AA0DACDB200F071 +:10880000F5FE50B198F82300CDE90005FA8923461A +:1088100039464846FEF79FFC91E711208FE72DE986 +:10882000F04F8BB01F4615460C4683460026FAF7DC +:1088300009FE28B10078222805D208200BB081E576 +:1088400043F20200FAE7B80801D00720F6E7032F49 +:1088500000D100274FF6FF79CCB1022D71D320460D +:10886000F8F744FD30B904EB0508A8F10100F8F76A +:108870003DFD08B11020E1E7AD1E38F8028CAAB228 +:108880002146484605F081FE40455AD1ADB21C490B +:10889000B80702D58889401C00E001201FFA80F843 +:1088A000F80701D08F8900E04F4605AA4146584697 +:1088B00005F0B5FB4FF0070A4FF00009FCB1204668 +:1088C00008E0408810283CD8361D304486B2AE42BD +:1088D00037D2A01902884245F3D352E09DF8170021 +:1088E00002074ED57CB304EB0608361DB8F80230FB +:1088F000B6B2102B25D89A19AA4222D802E040E03D +:1089000094020020B8F8002091421AD1C0061BD56D +:10891000CDE900A90DF1080C0AAAA11948468CE876 +:108920000700B8F800100022584605F0E6F910B12B +:10893000FEF7CDF982E7B8F80200BDF828108842AA +:1089400002D00B207AE704E0B8F80200304486B287 +:1089500006E0C00604D55846FEF730FC00288AD150 +:108960009DF81700BDF81A1020F010008DF81700C0 +:10897000BDF81700ADF80000FF235846009A05F037 +:10898000D2FC05A805F057FB18B9BDF81A10B9427A +:10899000A4D9042158460BF089FA040000D1FFDF66 +:1089A000A2895AB1CDE900A94D4600232146584677 +:1089B000FEF7D1FB0028BDD1A5813FE700203DE7B0 +:1089C0002DE9FF4F8BB01E4617000D464FF00004F7 +:1089D00012D0B00802D007200FB0B3E4032E00D1AC +:1089E00000265DB10846F8F778FC28B93888691E7A +:1089F0000844F8F772FC08B11020EDE7C74AB00749 +:108A000001D5D18900E00121F0074FF6FF7802D0AF +:108A1000D089401E00E0404686B206AA0B9805F0B9 +:108A2000FEFA4FF000094FF0070B0DF1140A38E081 +:108A30009DF81B00000734D5CDF80490CDF800B0A8 +:108A4000CDF80890CDE9039A434600220B9805F033 +:108A5000B6FB60BB05B3BDF814103A882144281951 +:108A6000091D8A4230D3BDF81E2020F8022BBDF824 +:108A7000142020F8022BCDE900B9CDE90290CDF801 +:108A800010A0BDF81E10BDF8143000220B9805F0A0 +:108A900096FB08B103209FE7BDF814002044001D99 +:108AA00084B206A805F0C7FA20B10A2806D0FEF75E +:108AB0000EF991E7BDF81E10B142B9D934B17DB1BC +:108AC0003888A11C884203D20C2085E7052083E763 +:108AD00022462946404605F058FD014628190180E6 +:108AE000A41C3C80002077E710B50446F8F7D7FBBC +:108AF00008B1102010BD8948C0892080002010BD19 +:108B0000F0B58BB00D4606461422002103A805F0EF +:108B1000BEFC01208DF80C008DF8100000208DF8AF +:108B20001100ADF814503046FAF78CFC48B10078CB +:108B3000222812D3042130460BF0B8F9040005D1E5 +:108B400003E043F202000BB0F0BDFFDF04F11400BC +:108B5000074604F0F4F8800601D40820F3E7207CEF +:108B6000022140F00100207409A80094CDE9011011 +:108B7000072203A930466368FEF7A2FA20B1217CE0 +:108B800021F001012174DEE729463046F9F791FC16 +:108B900008A9384604F0C2F800B1FFDFBDF8204054 +:108BA000172C01D2172000E02046A84201D92C46FC +:108BB00002E0172C00D2172421463046FFF713FBA2 +:108BC00021463046F9F799F90020BCE7F8B51C4674 +:108BD00015460E46069F0BF09AFA2346FF1DBCB2BF +:108BE00031462A4600940AF086FEF8BD70B50C4660 +:108BF00005460E220021204605F049FC0020208079 +:108C00002DB1012D01D0FFDF64E4062000E0052036 +:108C1000A0715FE410B548800878134620F00F007B +:108C2000001D20F0F00080300C4608701422194618 +:108C300004F1080005F001FC00F0DBFC374804609B +:108C400010BD2DE9F047DFF8D890491D064621F008 +:108C5000030117460C46D9F800000AF062FF050030 +:108C600000D1FFDF4FF000083560A5F800802146F5 +:108C7000D9F800000AF055FF050000D1FFDF75604C +:108C8000A5F800807FB104FB07F1091D0BD0D9F8CE +:108C900000000AF046FF040000D1FFDFB460C4F812 +:108CA0000080BDE8F087C6F80880FAE72DE9F041BA +:108CB0001746491D21F00302194D06460168144666 +:108CC00028680AF059FF2246716828680AF054FFA4 +:108CD0003FB104FB07F2121D03D0B16828680AF007 +:108CE0004BFF04200BF08AF8044604200BF08EF8AA +:108CF000201A012804D12868BDE8F0410AF006BF17 +:108D0000BDE8F08110B50C4605F058F900B1FFDF61 +:108D10002046BDE81040FDF7DABF000094020020B5 +:108D20001800002038B50C468288817B19B1418932 +:108D3000914200D90A462280C188121D90B26A462B +:108D40000AF0B2F8BDF80000032800D30320C1B236 +:108D5000208801F020F838BD38B50C468288817B28 +:108D600019B10189914200D90A462280C188121D99 +:108D700090B26A460AF098F8BDF80000022800D3C5 +:108D80000220C1B2208801F006F8401CC0B238BDF4 +:108D90002DE9FF5F82468B46F74814460BF103022C +:108DA000D0E90110CDE9021022F0030201A84FF42E +:108DB000907101920AF097FEF04E002C02D1F0491A +:108DC000019A8A60019901440191B57F05F101057D +:108DD00004D1E8B20CF098FD00B1FFDF019800EB80 +:108DE0000510C01C20F0030101915CB9707AB27AC1 +:108DF0001044C2B200200870B08C80B204F03DFF75 +:108E000000B1FFDF0198716A08440190214601A872 +:108E100000F084FF80460198C01C20F00300019000 +:108E2000B37AF27A717A04B100200AF052FF019904 +:108E300008440190214601A800F0B8FFCF48002760 +:108E40003D4690F801900CE0284600F04AFF0646A7 +:108E500081788088F9F7E8F871786D1C00FB01775C +:108E6000EDB24D45F0D10198C01C20F003000190F7 +:108E700004B100203946F9F7E2F8019900270844C7 +:108E80000190BE483D4690F801900CE0284600F065 +:108E900028FF0646C1788088FEF71BFC71786D1CA0 +:108EA00000FB0177EDB24D45F0D10198C01C20F0D8 +:108EB0000300019004B100203946FEF713FC01992C +:108EC0004FF0000908440190AC484D4647780EE049 +:108ED000284600F006FF0646807B30B106F1080008 +:108EE00002F09CF9727800FB02996D1CEDB2BD4254 +:108EF000EED10198C01C20F00300019004B10020C5 +:108F00009F494A78494602F08DF901990844019039 +:108F1000214601A800F0B8FE0198C01D20F007000E +:108F20000190DAF80010814204D3A0EB0B01B1F5F7 +:108F3000803F04DB4FF00408CAF8000004E0CAF8E0 +:108F40000000B8F1000F03D0404604B0BDE8F09F28 +:108F500084BB8C490020019A0EF044FEFBF714FA02 +:108F6000864C207F0090607F012825D0002328B305 +:108F70000022824800211030F8F73AFA00B1FFDFF2 +:108F80007E49E07F2031FEF759FF00B1FFDF7B48CB +:108F90004FF4F6720021443005F079FA7748042145 +:108FA000443080F8E91180F8EA11062180F8EB11CD +:108FB000032101710020C8E70123D8E702AAD8E7FE +:108FC00070B56E4C06464434207804EB4015E078CA +:108FD000083598B9A01990F8E80100280FD0A078BA +:108FE0000F2800D3FFDF20220021284605F04FFA8A +:108FF000687866F3020068700120E070284670BD52 +:109000002DE9F04105460C460027007805219046E1 +:109010003E46B1EB101F00D0FFDF287A50B1012887 +:109020000ED0FFDFA8F800600CB12780668000201A +:10903000BDE8F0810127092674B16888A08008E0A6 +:109040000227142644B16888A0802869E060A88AB5 +:109050002082287B2072E5E7A8F80060E7E730B5BA +:10906000464C012000212070617020726072032242 +:10907000A272E07261772177217321740521218327 +:109080001F216183607440A161610A21A177E077AB +:1090900039483B4DB0F801102184C07884F8220093 +:1090A0004FF4B06060626868C11C21F00301814226 +:1090B00000D0FFDF6868606030BD30B5304C1568A7 +:1090C000636810339D4202D20420136030BD2B4BE5 +:1090D0005D785A6802EB0512107051700320D08041 +:1090E000172090800120D0709070002090735878E5 +:1090F000401C5870606810306060002030BD70B552 +:1091000006461E480024457807E0204600F0E9FDA9 +:109110000178B14204D0641CE4B2AC42F5D1002025 +:1091200070BDF7B5074608780C4610B3FFF7E7FFA8 +:109130000546A7F12006202F06D0052E19D2DFE81C +:1091400006F00F383815270000F0D6FD0DB169780C +:1091500000E00021401AA17880B20844FF2808D816 +:10916000A07830B1A088022831D202E060881728A8 +:109170002DD20720FEBD000030610200B0030020A8 +:109180001C000020000000206E52463578000000D0 +:10919000207AE0B161881729EBD3A1881729E8D399 +:1091A000A1790029E5D0E1790029E2D0402804D94D +:1091B000DFE7242F0BD1207A48B161884FF6FB708E +:1091C000814202D8A188814201D90420D2E765B941 +:1091D000207802AA0121FFF770FF0028CAD1207869 +:1091E000FFF78DFF050000D1FFDF052E18D2DFE865 +:1091F00006F0030B0E081100A0786870A088E880C4 +:109200000FE06088A8800CE0A078A87009E0A07842 +:10921000E87006E054F8020FA8606068E86000E0BB +:10922000FFDF0020A6E71A2835D00DDC132832D244 +:10923000DFE800F01B31203131272723252D313184 +:1092400029313131312F0F00302802D003DC1E28A4 +:1092500021D1072070473A3809281CD2DFE800F0F6 +:10926000151B0F1B1B1B1B1B07000020704743F225 +:109270000400704743F202007047042070470D203D +:1092800070470F2070470820704711207047132047 +:109290007047062070470320704710B5007800F033 +:1092A000010009F0F3FDBDE81040BCE710B50078FF +:1092B00000F0010009F0F3FDBDE81040B3E70EB582 +:1092C000017801F001018DF80010417801F00101F1 +:1092D0008DF801100178C1F340018DF8021041783A +:1092E000C1F340018DF80310017889088DF804104E +:1092F000417889088DF8051081788DF80610C178BD +:109300008DF8071000798DF80800684608F0FDFD1B +:10931000FFF789FF0EBD2DE9FC5FDFF8F883FE4CF7 +:1093200000264FF490771FE0012000F082FD01201D +:10933000FFF746FE05463946D8F808000AF0F1FB6B +:10934000686000B9FFDF686808F0AAFCB0B1284681 +:10935000FAF75EFB284600F072FD28B93A466968C4 +:10936000D8F808000AF008FC94F9E9010428DBDACF +:1093700002200AF043FD07460025AAE03A46696844 +:10938000D8F808000AF0F8FBF2E7B8F802104046F7 +:10939000491C89B2A8F80210B94201D300214180CA +:1093A0000221B8F802000AF081FD002866D0B8F862 +:1093B0000200694609F0CFFCFFF735FF00B1FFDF7F +:1093C0009DF80000019078B1B8F802000AF0B1FEF3 +:1093D0005FEA000900D1FFDF48460AF020F918B122 +:1093E000B8F8020002F0E4F9B8F802000AF08FFEC3 +:1093F0005FEA000900D1FFDF48460AF008F9E8BB40 +:109400000321B8F802000AF051FD5FEA000B4BD1CE +:10941000FFDF49E0DBF8100010B10078FF284DD0E5 +:10942000022000F006FD0220FFF7CAFD82464846F2 +:109430000AF0F9F9CAF8040000B9FFDFDAF804000D +:109440000AF0C1FA002100900170B8F802105046ED +:10945000AAF8021002F0B2F848460AF0B6FA00B9CB +:10946000FFDF019800B10126504600F0E8FC18B972 +:109470009AF80100000705D5009800E027E0CBF836 +:10948000100011E0DBF8101039B10878401C10F022 +:10949000FF00087008D1FFDF06E0002211464846B1 +:1094A00000F0F0FB00B9FFDF94F9EA01022805DBC8 +:1094B000B8F8020002F049F80028ABD194F9E901AC +:1094C000042804DB48460AF0E4FA00B101266D1CCA +:1094D000EDB2BD4204D294F9EA010228BFF655AFBD +:1094E000002E7FF41DAFBDE8FC5F032000F0A1BC9F +:1094F00010B5884CE06008682061AFF2E510F9F71C +:10950000E4FC607010BD844800214438017081483B +:10951000017082494160704770B505464FF0805038 +:109520000C46D0F8A410491C05D1D0F8A810C943A6 +:109530000904090C0BD050F8A01F01F0010129709B +:10954000416821608068A080287830B970BD06210C +:1095500020460DF0CCFF01202870607940F0C0005B +:10956000607170BD70B54FF080540D46D4F8801016 +:10957000491C0BD1D4F88410491C07D1D4F88810A9 +:10958000491C03D1D4F88C10491C0CD0D4F880109D +:109590000160D4F884104160D4F888108160D4F858 +:1095A0008C10C16002E010210DF0A1FFD4F89000F2 +:1095B000401C0BD1D4F89400401C07D1D4F898007B +:1095C000401C03D1D4F89C00401C09D054F8900FE3 +:1095D000286060686860A068A860E068E86070BDA6 +:1095E0002846BDE8704010210DF081BF4A4800793F +:1095F000E6E470B5484CE07830B3207804EB4010D6 +:10960000407A00F00700204490F9E801002800DCCF +:10961000FFDF2078002504EB4010407A00F00700BF +:10962000011991F8E801401E81F8E8012078401CFA +:10963000C0B220700F2800D12570A078401CA07007 +:109640000DF0D4FDE57070BDFFDF70BD3EB5054681 +:1096500003210AF02BFC044628460AF058FD054673 +:1096600004B9FFDF206918B10078FF2800D1FFDFBF +:1096700001AA6946284600F005FB60B9FFDF0AE051 +:10968000002202A9284600F0FDFA00B9FFDF9DF88C +:10969000080000B1FFDF9DF80000411E8DF80010AA +:1096A000EED220690199884201D1002020613EBD9F +:1096B00070B50546A0F57F400C46FF3800D1FFDFAE +:1096C000012C01D0FFDF70BDFFF790FF040000D137 +:1096D000FFDF207820F00F00401D20F0F000503018 +:1096E000207065800020207201202073BDE870404A +:1096F0007FE72DE9F04116460D460746FFF776FF56 +:10970000040000D1FFDF207820F00F00401D20F082 +:10971000F00005E01C000020F403002048140020A5 +:109720005030207067800120207228682061A8884E +:10973000A0822673BDE8F0415BE77FB5FFF7DFFC51 +:10974000040000D1FFDF02A92046FFF7EBFA05462F +:1097500003A92046FFF700FB8DF800508DF80100AB +:10976000BDF80800001DADF80200BDF80C00001D9A +:10977000ADF80400E088ADF80600684609F070FB1B +:10978000002800D0FFDF7FBD2DE9F05FFC4E814651 +:10979000307810B10820BDE8F09F4846F7F77FFD0C +:1097A00008B11020F7E7F74C207808B9FFF757FC0D +:1097B000A17A607A4D460844C4B200F09DFAA042F6 +:1097C00007D2201AC1B22A460020FFF776FC0028F3 +:1097D000E1D17168EB48C91C002721F003017160D9 +:1097E000B3463E463D46BA463C4690F801800AE004 +:1097F000204600F076FA4178807B0E4410FB01553C +:10980000641CE4B27F1C4445F2D10AEB870000EBF4 +:10981000C600DC4E00EB85005C46F17A012200EBCD +:109820008100DBF80410451829464846FFF7B0FAD6 +:10983000070012D00020FFF762FC05000BD005F1F5 +:109840001300616820F00300884200D0FFDF7078C9 +:10985000401E7070656038469DE7002229464846E4 +:10986000FFF796FA00B1FFDFD9F8000060604FF60D +:10987000FF7060800120207000208CE72DE9F0410E +:109880000446BF4817460D46007810B10820BDE8D1 +:10989000F0810846F7F7DDFC08B11020F7E7B94E74 +:1098A000307808B9FFF7DBFB601E1E2807D8012CB3 +:1098B00023D12878FE2820D8B0770020E7E7A4F14C +:1098C00020001F2805D8E0B23A462946BDE8F041FD +:1098D00027E4A4F140001F2805D829462046BDE80A +:1098E000F04100F0D4BAA4F1A0001F2805D8294601 +:1098F0002046BDE8F04100F006BB0720C7E72DE990 +:10990000F05F81460F460846F7F7C9FC48B948465C +:10991000F7F7E3FC28B909F1030020F003014945FA +:1099200001D0102037E797484FF0000B4430817882 +:1099300069B14178804600EB411408343E883A46CC +:109940000021204600F089FA050004D027E0A7F89E +:1099500000B005201FE7B9F1000F24D03888B042CD +:1099600001D90C251FE0607800F00700824600F066 +:1099700060FA08EB0A063A4696F8E8014946401CA8 +:1099800086F8E801204600F068FA054696F8E801F6 +:10999000401E86F8E801032000F04BFA2DB10C2D93 +:1099A00001D0A7F800B02846F5E6754F5046BAF149 +:1099B000010F25D002280DD0BAF1030F35D0FFDFFB +:1099C00098F801104046491CC9B288F801100F29C7 +:1099D00037D038E0606828B16078000702D460882A +:1099E000FFF734FE98F8EA014446012802D178785E +:1099F000F9F78AFA94F9EA010428E1DBFFDFDFE7EF +:109A0000616821B14FF49072B8680AF0B5F898F81F +:109A1000E9014446032802D17878F9F775FA94F9F8 +:109A2000E9010428CCDBFFDFCAE76078C00602D575 +:109A30006088FFF70BFE98F9EB010628C0DBFFDF1B +:109A4000BEE780F801B08178491E88F8021096F8C8 +:109A5000E801401C86F8E801A5E770B50C4605460C +:109A6000F7F7F7FB18B92046F7F719FC08B11020F3 +:109A700070BD28460BF07FFF207008B1002070BD3C +:109A8000042070BD70B505460BF08EFFC4B22846A9 +:109A9000F7F723FC08B1102070BD35B128782C7081 +:109AA00018B1A04201D0072070BD2046FDF77EFE10 +:109AB000052805D10BF07BFF012801D0002070BDE7 +:109AC0000F2070BD70B5044615460E460846F7F7E0 +:109AD000C0FB18B92846F7F7E2FB08B1102070BDAB +:109AE000022C03D0102C01D0092070BD2A4631462B +:109AF00020460BF086FF0028F7D0052070BD70B51A +:109B000014460D460646F7F7A4FB38B92846F7F782 +:109B1000C6FB18B92046F7F7E0FB08B1102070BD6E +:109B20002246294630460BF06EFF0028F7D007206A +:109B300070BD3EB50446F7F7B2FB08B110203EBD3C +:109B4000684608F053F9FFF76EFB0028F7D19DF83F +:109B500006002070BDF808006080BDF80A00A080F3 +:109B600000203EBD70B505460C460846F7F7B5FB2C +:109B700020B95CB12068F7F792FB28B1102070BDC6 +:109B80001C000020B0030020A08828B121462846F0 +:109B9000BDE87040FDF762BE0920F0E770B50546EC +:109BA0000C460846F7F755FBA0BB681E1E280ED8CA +:109BB000032D01D90720E2E705B9FFDFFE4800EBDE +:109BC000850050F8041C2046BDE870400847A5F108 +:109BD00020001F2805D821462846BDE87040FAF726 +:109BE00042BBA5F160001F2805D821462846BDE8E4 +:109BF0007040F8F7DABCF02D0DD0F12D15D0BF2D47 +:109C0000D8D1A078218800F0010001F08DFB98B137 +:109C10000020B4E703E0A068F7F71BFB08B11020B1 +:109C2000ADE7204609F081F902E0207809F0A0F9BB +:109C3000BDE87040FFF7F7BA0820A0E770B504460A +:109C40000D460846F7F72BFB30B9601E1E280FD8CB +:109C50002846F7F7FEFA08B1102090E7012C03D050 +:109C6000022C01D0032C01D1062088E7072086E7CB +:109C7000A4F120001F28F9D829462046BDE87040ED +:109C8000FAF762BB09F092BC38B50446CB48007BBA +:109C900000F00105F9B904F01DFC0DB1226800E0E7 +:109CA0000022C7484178C06807F06DFDC4481030F5 +:109CB000C0788DF8000010B1012802D004E0012026 +:109CC00000E000208DF80000684608F0FFF8BA4870 +:109CD000243808F0B5FE002D02D02068283020601E +:109CE00038BD30B5B54D04466878A04200D8FFDFD6 +:109CF000686800EB041030BD70B5B04800252C46F4 +:109D0000467807E02046FFF7ECFF4078641C2844C3 +:109D1000C5B2E4B2B442F5D1284630E72DE9F041AE +:109D20000C4607464FF0000800F01FF90646FF28D2 +:109D300001D94FF013083868C01C20F003023A60C4 +:109D400054EA080421D19D48F3B2072128300DF0D0 +:109D5000DBFD09E0072C10D2DFE804F00604080858 +:109D60000A040600974804E0974802E0974800E09C +:109D700097480DF0E9FD054600E0FFDFA54200D061 +:109D8000FFDF641CE4B2072CE4D3386800EB061054 +:109D9000386040467BE5021D5143452900D24521EC +:109DA0000844C01CB0FBF2F0C0B270472DE9FC5F64 +:109DB000064682484FF000088B464746444690F8D6 +:109DC000019022E02046FFF78CFF050000D1FFDF65 +:109DD000687869463844C7B22846FEF7A3FF824632 +:109DE00001A92846FEF7B8FF0346BDF80400524615 +:109DF000001D81B2BDF80000001D80B20AF0D4F849 +:109E00006A78641C00FB0288E4B24C45DAD1306801 +:109E1000C01C20F003003060BBF1000F00D0002018 +:109E2000424639460AF0CEF8316808443060BDE851 +:109E3000FC9F6249443108710020C87070475F4937 +:109E40004431CA782AB10A7801EB421108318142C3 +:109E500001D001207047002070472DE9F0410646EF +:109E60000078154600F00F0400201080601E0F4699 +:109E7000052800D3FFDF50482A46183800EB84003D +:109E8000394650F8043C3046BDE8F04118472DE90A +:109E9000F0414A4E0C46402806D0412823D04228A3 +:109EA0002BD0432806D123E0A07861780D18E17803 +:109EB000814201D90720EAE42078012801D9132042 +:109EC000E5E4FF2D08D80BF009FF07460DF046F931 +:109ED000381A801EA84201DA1220D8E42068B06047 +:109EE000207930730DE0BDE8F041084600F078B805 +:109EF00008780228DED8307703E008780228D9D81D +:109F000070770020C3E4F8B500242C4DA02805D0BC +:109F1000A12815D0A22806D00720F8BD087800F0A7 +:109F20000100E8771FE00E4669463046FDF73DFD2B +:109F30000028F2D130882884B07885F8220012E019 +:109F400008680921F82801D3820701D00846F8BD26 +:109F50006A7C02F00302012A04D16A8BD73293B2E1 +:109F60008342F3D868622046F8BD2DE9F047DFF858 +:109F70004C900026344699F8090099F80A2099F87F +:109F800001700244D5B299F80B20104400F0FF088C +:109F900008E02046FFF7A5FE817B407811FB0066B4 +:109FA000641CE4B2BC42F4D199F8091099F80A0093 +:109FB0002944294441440DE054610200B0030020CB +:109FC0001C0000206741000045B30000DD2F0000A9 +:109FD000FB56010000B1012008443044BDE8F08781 +:109FE00038B50446407800F00300012803D0022869 +:109FF0000BD0072038BD606858B1F7F777F9D0B9B2 +:10A000006068F7F76AF920B915E06068F7F721F999 +:10A0100088B969462046FCF729F80028EAD160781B +:10A0200000F00300022808D19DF8000028B1606804 +:10A03000F7F753F908B1102038BD6189F8290DD818 +:10A04000208988420AD8607800F003020A48012A71 +:10A0500006D1D731426A89B28A4201D2092038BD7D +:10A0600094E80E0000F1100585E80E000AB9002101 +:10A070000183002038BD0000B00300202DE9F0412D +:10A08000074614468846084601F08AFD064608EB56 +:10A0900088001C22796802EBC0000D18688C58B14A +:10A0A0004146384601F08BFD014678680078C200D1 +:10A0B000082305F120000CE0E88CA8B141463846A1 +:10A0C00001F084FD0146786808234078C20005F15C +:10A0D000240009F0A8FD38B1062121726681D0E97B +:10A0E0000010C4E9031009E0287809280BD00520E6 +:10A0F000207266816868E060002028702046BDE814 +:10A10000F04101F02EBD072020726681F4E72DE9B1 +:10A11000F04116460D460746406801EB85011C22BA +:10A1200002EBC1014418204601F072FD40B100214C +:10A13000708865F30F2160F31F4106200DF0BEFC0F +:10A1400009202070324629463846BDE8F04195E79F +:10A150002DE9F0410E46074600241C21F07816E058 +:10A1600004EB8403726801EBC303D25C6AB1FFF7AE +:10A170003DFA050000D1FFDF6F802A4621463046B8 +:10A18000FFF7C5FF0120BDE8F081641CE4B2A042E6 +:10A19000E6D80020F7E770B5064600241C21C078F9 +:10A1A0000AE000BF04EB8403726801EBC303D51817 +:10A1B0002A782AB1641CE4B2A042F3D8402070BDD2 +:10A1C00028220021284604F062F9706880892881DD +:10A1D000204670BD70B5034600201C25DC780CE0DD +:10A1E00000EB80065A6805EBC6063244167816B1B5 +:10A1F000128A8A4204D0401CC0B28442F0D8402067 +:10A2000070BDF0B5044600201C26E5780EE000BFC6 +:10A2100000EB8007636806EBC7073B441F788F425B +:10A2200002D15B78934204D0401CC0B28542EFD883 +:10A230004020F0BD0078032801D0002070470120A5 +:10A2400070470078022801D0002070470120704735 +:10A250000078072801D000207047012070472DE9C1 +:10A26000F041064688461078F1781546884200D3BA +:10A27000FFDF2C781C27641CF078E4B2A04201D8E0 +:10A28000201AC4B204EB8401706807EBC1010844D2 +:10A29000017821B14146884708B12C7073E72878CE +:10A2A000A042E8D1402028706DE770B514460B88B5 +:10A2B0000122A240134207D113430B8001230A223B +:10A2C000011D09F07AFC047070BD2DE9FF4F81B0CB +:10A2D0000878DDE90E7B9A4691460E4640072CD45D +:10A2E000019809F026FF040000D1FFDF07F1040800 +:10A2F00020461FFA88F109F065F8050000D1FFDF5C +:10A30000204629466A4609F0B0FA0098A0F8037082 +:10A31000A0F805A0284609F056FB017869F306016C +:10A320006BF3C711017020461FFA88F109F08DF810 +:10A3300000B9FFDF019807F094F906EB0900017FEF +:10A34000491C017705B0BDE8F08F2DE9F84F0E46A6 +:10A350009A4691460746032109F0A8FD0446008D60 +:10A36000DFF8B885002518B198F80000B0421ED17A +:10A37000384609F0DEFE070000D1FFDF09F10401D5 +:10A38000384689B209F01EF8050010D03846294633 +:10A390006A4609F06AFA009800210A460180817035 +:10A3A00007F01CFA0098C01DCAF8000021E098F8D8 +:10A3B0000000B04216D104F1260734F8341F012002 +:10A3C00000FA06F911EA090F00D0FFDF2088012307 +:10A3D00040EA090020800A22391D384609F008FCAD +:10A3E000067006E0324604F1340104F12600FFF75E +:10A3F0005CFF0A2188F800102846BDE8F88FFEB5FA +:10A4000015460C46064602AB0C220621FFF79DFFBF +:10A41000002827D00299607812220A70801C4870A8 +:10A4200008224A80A07002982988052381806988C3 +:10A43000C180A9880181E988418100250C20CDE9EE +:10A440000005062221463046FFF73FFF294600223D +:10A4500066F31F41F02310460DF086FA6078801CE9 +:10A4600060700120FEBDFEB514460D46062206466C +:10A4700002AB1146FFF769FF002812D0029B1320A0 +:10A4800000211870A8785870022058809C800620FF +:10A49000CDE900010246052329463046FFF715FFA6 +:10A4A0000120FEBD2DE9FE430C46804644E002AB90 +:10A4B0000E2207214046FFF748FF002841D0606880 +:10A4C0001C2267788678BF1C06EB860102EBC1016F +:10A4D000451802981421017047700A214180698A49 +:10A4E0000181E98A4181A9888180A98981813046D9 +:10A4F00001F056FB029905230722C8806F700420E3 +:10A50000287000250E20CDE9000521464046FFF7C2 +:10A51000DCFE294666F30F2168F31F41F023002279 +:10A5200006200DF021FA6078FD49801C6070626899 +:10A530002046921CFFF793FE606880784028B6D1D1 +:10A540000120BDE8FE83FEB50D46064638E002ABAD +:10A550000E2207213046FFF7F8FE002835D0686844 +:10A560001C23C17801EB810203EBC202841802981C +:10A5700015220270627842700A224280A2894281CA +:10A58000A2888281084601F00BFB01460298818077 +:10A59000618AC180E18A0181A088B8B10020207061 +:10A5A00000210E20CDE9000105230722294630466F +:10A5B000FFF78BFE6A68DB492846D21CFFF74FFE87 +:10A5C0006868C0784028C2D10120FEBD0620E6E7B9 +:10A5D0002DE9FE430C46814644E0204601F002FB93 +:10A5E000D0B302AB082207214846FFF7AEFE002891 +:10A5F000A7D060681C2265780679AD1C06EB860141 +:10A6000002EBC10147180298B7F8108006210170CB +:10A61000457004214180304601F0C2FA014602989B +:10A6200005230722C180A0F804807D7008203870BF +:10A630000025CDE9000521464846FFF746FE29469C +:10A6400066F30F2169F31F41F023002206200DF06D +:10A650008BF96078801C60706268B3492046121DD7 +:10A66000FFF7FDFD606801794029B6D1012068E758 +:10A670002DE9F34F83B00D4691E0284601F0B2FA80 +:10A6800000287DD068681C2290F806A00AEB8A0199 +:10A6900002EBC10144185146284601F097FAA1780F +:10A6A000CB0069684978CA00014604F1240009F02A +:10A6B000D6FA07468188E08B4FF00009091A8EB25E +:10A6C00008B1C84607E04FF00108504601F053FAC0 +:10A6D00008B9B61CB6B2208BB04200D80646B346C5 +:10A6E00002AB324607210398FFF72FFE060007D082 +:10A6F000B8F1000F0BD0504601F03DFA10B106E062 +:10A7000000201FE60299B8884FF0020908800196E0 +:10A71000E28B3968ABEB09001FFA80F80A44039812 +:10A720004E46009209F005FDDDE90021F61D434685 +:10A73000009609F014F9E08B404480B2E083B988B8 +:10A74000884201D1012600E00026CDE900B6238A27 +:10A75000072229460398FFF7B8FD504601F00BFA8F +:10A7600010B9E089401EE08156B1A078401CA0706D +:10A770006868E978427811FB02F1CAB2012300E06F +:10A7800007E081690E3009F018FA80F800A0002077 +:10A79000E0836A6865492846921DFFF760FD686896 +:10A7A000817940297FF469AF0120CBE570B5064679 +:10A7B00048680D4614468179402910D104EB840184 +:10A7C0001C2202EBC101084401F043FA002806D024 +:10A7D0006868294684713046BDE8704048E770BD1E +:10A7E000FEB50C460746002645E0204601F0FAF982 +:10A7F000D8B360681C22417901EB810102EBC101F1 +:10A800004518688900B9FFDF02AB082207213846E6 +:10A81000FFF79BFD002833D00299607816220A705A +:10A82000801C4870042048806068407901F0B8F9C5 +:10A83000014602980523072281806989C18008208A +:10A84000CDE9000621463846FFF73FFD6078801CC1 +:10A850006070A88969890844B0F5803F00D3FFDFA4 +:10A86000A88969890844A8816E81626830492046B8 +:10A87000521DFFF7F4FC606841794029B5D10120F1 +:10A88000FEBD30B5438C458BC3F3C704002345B1EF +:10A89000838B641EED1AC38A6D1E1D4495FBF3F372 +:10A8A000E4B22CB1008918B1A04200D8204603447C +:10A8B0004FF6FF70834200D3034613800C7030BD07 +:10A8C0002DE9FC41074616460D46486802EB860115 +:10A8D0001C2202EBC10144186A4601A92046FFF779 +:10A8E000D0FFA089618901448AB2BDF8001091426D +:10A8F00012D0081A00D5002060816868407940288D +:10A900000AD1204601F09BF9002805D06868294645 +:10A9100046713846FFF764FFBDE8FC813000002037 +:10A9200035A2000043A2000051A2000053BC000069 +:10A930003FBC00002DE9FE4F0F468146154650886A +:10A94000032109F0B3FA0190B9F8020001F01BF9F4 +:10A9500082460146019801F045F9002824D001986B +:10A960001C2241680AEB8A0002EBC0000C1820464A +:10A9700001F04EF9002817D1B9F80000E18A8842A9 +:10A980000ED8A18961B1B8420ED100265146019876 +:10A9900001F015F9218C01EB0008608B30B114E057 +:10A9A000504601F0E8F8A0B3BDE8FE8F504601F034 +:10A9B000E2F808B1678308E0022FF5D3B9F8040084 +:10A9C0006083618A884224D80226B81B87B2B8F80F +:10A9D0000400A28B801A002814DD874200DA384672 +:10A9E0001FFA80FB688869680291D8F800100A4451 +:10A9F000009209F08CFBF61D009A5B4602990096C6 +:10AA000008F079FFA08B384480B2A083618B884224 +:10AA100007D96888019903B05246BDE8F04F01F0AC +:10AA200035B91FD14FF009002872B9F802006881CA +:10AA3000D8E90010C5E90410608BA881284601F010 +:10AA400090F85146019801F0BAF8014601980823A0 +:10AA500040680078C20004F1200009F0E4F800200A +:10AA6000A0836083504601F086F810B9A089401E8B +:10AA7000A0816888019903B00AF0FF02BDE8F04F99 +:10AA80001EE72DE9F041064615460F461C461846BE +:10AA9000F6F7DFFB18B92068F6F701FC10B11020BB +:10AAA000BDE8F0817168688C0978B0EBC10F01D303 +:10AAB0001320F5E73946304601F081F80146706809 +:10AAC00008230078C20005F1200009F076F8D4E9E7 +:10AAD0000012C0E900120020E2E710B5044603218D +:10AAE00009F0E4F90146007800F00300022805D0DF +:10AAF0002046BDE8104001F1140280E48A8A204615 +:10AB0000BDE81040AFE470B50446032109F0CEF96A +:10AB1000054601462046FFF75BFD002816D0294672 +:10AB20002046FFF75DFE002810D029462046FFF79B +:10AB30000AFD00280AD029462046FFF7B3FC00286A +:10AB400004D029462046BDE8704091E570BD2DE94E +:10AB5000F0410C4680461EE0E178427811FB02F19C +:10AB6000CAB2816901230E3009F05DF80778606888 +:10AB70001C22C179491EC17107EB8701606802EB95 +:10AB8000C10146183946204601F02CF818B130466C +:10AB900001F037F820B16068C1790029DCD17FE786 +:10ABA000FEF724FD050000D1FFDF0A202872384699 +:10ABB00000F0F6FF68813946204601F007F80146AB +:10ABC000606808234078C20006F1240009F02BF8E1 +:10ABD000D0E90010C5E90310A5F80280284600F06E +:10ABE000C0FFB07800B9FFDFB078401EB07057E703 +:10ABF00070B50C460546032109F058F90146406836 +:10AC0000C2792244C2712846BDE870409FE72DE911 +:10AC1000FE4F8246507814460F464FF00008002839 +:10AC20004FD0012807D0022822D0FFDF2068B8606B +:10AC30006068F860B8E602AB0E2208215046FFF7C4 +:10AC400084FB0028F2D00298152105230170217899 +:10AC500041700A214180C0F80480C0F80880A0F843 +:10AC60000C80628882810E20CDE90008082221E054 +:10AC7000A678304600F094FF054606EB86012C22AC +:10AC8000786802EBC1010822465A02AB11465046D1 +:10AC9000FFF75BFB0028C9D00298072101702178DB +:10ACA00041700421418008218580C680CDE90018CB +:10ACB00005230A4639465046FFF707FB87F8088008 +:10ACC00072E6A678022516B1022E13D0FFDF2A1DE8 +:10ACD000914602AB08215046FFF737FB0028A5D06C +:10ACE00002980121022E01702178417045808680F2 +:10ACF00002D005E00625EAE7A188C180E18801814C +:10AD0000CDE900980523082239465046D4E710B50E +:10AD10000446032109F0CAF8014600F10802204662 +:10AD2000BDE8104073E72DE9F04F0F4605468DB0A2 +:10AD300014465088032109F0B9F84FF000088DF847 +:10AD400014800646ADF81680042F7BD36A78002A5B +:10AD500078D028784FF6FF794FF01C0A132834D0AA +:10AD600008DC012871D006284AD007286ED01228A6 +:10AD70000ED106E014286AD0152869D0162807D10C +:10AD8000AAE10C2F04D1307800F00301022907D08A +:10AD9000CDF80880CDF80C8068788DF808004CE07C +:10ADA00040F0080030706878B07001208DF8140011 +:10ADB000A888ADF81800E888ADF81A002889ADF821 +:10ADC0001C006889ADF81E0011E1B078904239D1BD +:10ADD0003078010736D5062F34D120F008003070C6 +:10ADE0006088414660F31F4100200CF067FE02209E +:10ADF0008DF81400ADF81890A888ADF81A00F6E0A8 +:10AE0000082F1FD1A888EF88814600F0BCFE80463D +:10AE10000146304600F0E6FE18B1404600F0ABFEB9 +:10AE2000B8B1FC48D0E90010CDE902106878ADF85F +:10AE30000C908DF80800ADF80E70608802AA3146BB +:10AE4000FFF7E5FE0DB0BDE8F08FB6E01EE041E093 +:10AE5000ECE0716808EB88002C2202EBC000085A75 +:10AE6000B842EFD1EB4802AAD0E90210CDE90210B6 +:10AE700068788DF8080008F0FF058DF80A506088A2 +:10AE80003146FFF7C4FE224629461FE0082FD9D1DC +:10AE9000B5F80480E88800F076FE074601463046A3 +:10AEA00000F0A0FE0028CDD007EB870271680AEB06 +:10AEB000C2000844028A4245C4D101780829C1D1A0 +:10AEC000407869788842BDD1F9B222463046FFF712 +:10AED0001EF9B7E70E2F7FF45BAFE9886F898B46C9 +:10AEE000B5F808903046FFF775F9ABF140014029FD +:10AEF00001D309204AE0B9F1170F01D3172F01D26E +:10AF00000B2043E040280ED000EB800271680AEB72 +:10AF1000C20008440178012903D140786978884249 +:10AF200090D00A2032E03046FFF735F9014640283C +:10AF30002BD001EB810372680AEBC30002EB00081F +:10AF4000012288F800206A7888F801207068AA88B1 +:10AF50004089B84200D93846AD8903232372A282C2 +:10AF6000E7812082A4F80C906582084600F018FE64 +:10AF70006081A8F81490A8F81870A8F80E50A8F8E6 +:10AF800010B0204600F0EDFD5CE7042005212172A1 +:10AF9000A4F80A80E081012121739E49D1E90421AE +:10AFA000CDE9022169788DF80810ADF80A006088B3 +:10AFB00002AA3146FFF72BFEE3E7062F89D3B078CC +:10AFC00090421AD13078010717D520F00800307070 +:10AFD0006088414660F31F4100200CF06FFD0220A5 +:10AFE0008DF81400A888ADF81800ADF81A906088A4 +:10AFF000224605A9F9F7E3F824E704213046FFF7D4 +:10B0000000F905464028BFD0022083030090224665 +:10B010002946304600F003FE4146608865F30F2163 +:10B0200060F31F4106200CF049FD0BE70E2FABD15A +:10B0300004213046FFF7E5F881464028A4D0414678 +:10B04000608869F30F2160F31F4106200CF036FD84 +:10B05000A8890B906889099070682F894089B84247 +:10B0600000D938468346B5F80680A8880A90484635 +:10B0700000F096FD60810B9818B1022000900B9BA8 +:10B0800024E0B8F1170F1ED3172F1CD30420207211 +:10B0900009986082E781A4F810B0A4F80C8009EB4D +:10B0A000890271680AEBC2000D18DDE90913A5F8E1 +:10B0B0001480A5F818B0E9812B82204600F051FDDC +:10B0C00006202870BEE601200B2300902246494648 +:10B0D000304600F0A4FDB5E6082F8DD1A988304692 +:10B0E000FFF778F80746402886D000F044FD002896 +:10B0F0009BD107EB870271680AEBC20008448046C7 +:10B1000000F086FD002890D1ED88B8F80E002844A4 +:10B11000B0F5803F05D360883A46314600F0B6FD71 +:10B1200090E6002DCED0A8F80E0060883A46314651 +:10B13000FFF73CFB08202072384600F031FD6081AB +:10B14000A5811EE72DE9F05F0C4601281FD09579F7 +:10B1500092F8048092F8056005EB85011F2202EB4E +:10B16000C10121F0030B08EB060111FB05F14FF6BD +:10B17000FF7202EAC10909F1030115FB0611264F0E +:10B1800021F0031ABB6840B101283ED125E0616877 +:10B19000E57891F800804E78DEE75946184608F0C9 +:10B1A000C0FC606000B9FFDF5A460021606803F010 +:10B1B0006EF9E5705146B86808F0B3FC6168486103 +:10B1C00000B9FFDF6068426902EB090181616068D4 +:10B1D00080F800806068467017E0606852464169F8 +:10B1E000184608F0C9FC5A466168B86808F0C4FC03 +:10B1F000032008F003FE0446032008F007FE201A8F +:10B20000012802D1B86808F081FC0BEB0A00BDE808 +:10B21000F09F000060610200300000200246002123 +:10B2200002208FE7F7B5FF4C0A20164620700098E1 +:10B2300060B100254FEA0D0008F055FC0021A17017 +:10B240006670002D01D10099A160FEBD012500208E +:10B25000F2E770B50C46154638220021204603F06F +:10B2600016F9012666700A22002104F11C0003F081 +:10B270000EF905B9FFDF297A207861F3010020700B +:10B28000A87900282DD02A4621460020FFF75AFF32 +:10B2900061684020E34A88706168C870616808711D +:10B2A000616848716168887161682888088161688F +:10B2B00068884881606886819078002811D061682C +:10B2C0000620087761682888C885616828884886CC +:10B2D00060680685606869889288018681864685EF +:10B2E000828570BDC878002802D00022012029E79D +:10B2F000704770B50546002165F31F4100200CF032 +:10B30000DDFB0321284608F0D1FD040000D1FFDF5A +:10B3100021462846FEF71CFF002804D0207840F084 +:10B3200010002070012070BD70B505460C4603204A +:10B3300008F056FD08B1002070BDBA4885708480C1 +:10B34000012070BD2DE9FF4180460E460F0CFEF72F +:10B350004DF9050007D06F800321384608F0A6FD9F +:10B36000040008D106E004B03846BDE8F0411321DE +:10B37000F9F750BBFFDF5FEA080005D0B8F1060F10 +:10B3800018D0FFDFBDE8FF8120782A4620F00800B2 +:10B3900020700020ADF8020002208DF800004FF66A +:10B3A000FF70ADF80400ADF8060069463846F8F7BE +:10B3B00006FFE7E7C6F3072101EB81021C23606863 +:10B3C00003EBC202805C042803D008280AD0FFDF08 +:10B3D000D8E7012000904FF440432A46204600F071 +:10B3E0001EFCCFE704B02A462046BDE8F041FEF738 +:10B3F0008EBE2DE9F05F05464089002790460C4639 +:10B400003E46824600F0BFFB8146287AC01E0828CF +:10B410006BD2DFE800F00D04192058363C47722744 +:10B420001026002C6CD0D5E90301C4E902015CE0D0 +:10B4300070271226002C63D00A2205F10C0104F1BA +:10B44000080002F0FAFF50E071270C26002C57D0BC +:10B45000E868A06049E0742710269CB3D5E9030191 +:10B46000C4E902016888032108F020FD8346FEF745 +:10B47000BDF802466888508049465846FEF7FEFDF2 +:10B4800033E075270A26ECB1A88920812DE07627C4 +:10B490001426BCB105F10C0004F1080307C883E8C9 +:10B4A000070022E07727102664B1D5E90301C4E93B +:10B4B00002016888032108F0F9FC01466888FFF75B +:10B4C00046FB12E01CE073270826CCB168880321F4 +:10B4D00008F0ECFC01460078C00606D56888FEF747 +:10B4E00037FE10B96888F8F777FAA8F800602CB131 +:10B4F0002780A4F806A066806888A080002086E6E1 +:10B50000A8F80060FAE72DE9FC410C461E461746F4 +:10B510008046032108F0CAFC05460A2C0AD2DFE85F +:10B5200004F005050505050509090907042303E0DD +:10B53000062301E0FFDF0023CDE9007622462946FD +:10B540004046FEF7C2FEBDE8FC81F8B50546A0F511 +:10B550007F40FF382BD0284608F0D9FD040000D1E9 +:10B56000FFDF204608F05FF9002821D001466A4637 +:10B57000204608F07AF900980321B0F805602846C3 +:10B5800008F094FC0446052E13D0304600F0FBFA78 +:10B5900005460146204600F025FB40B1606805EBFA +:10B5A00085013E2202EBC101405A002800D0012053 +:10B5B000F8BD007A0028FAD00020F8BDF8B504469E +:10B5C000408808F0A4FD050000D1FFDF6A46284648 +:10B5D000616800F0C4FA01460098091F8BB230F888 +:10B5E000032F0280428842800188994205D1042AB3 +:10B5F00008D0052A20D0062A16D022461946FFF781 +:10B6000099F9F8BD001D0E46054601462246304612 +:10B61000F6F739FF0828F4D1224629463046FCF7D0 +:10B6200064F9F8BD30000020636864880A46011D93 +:10B630002046FAF789F9F4E72246001DFFF773FB6D +:10B64000EFE770B50D460646032108F02FFC040015 +:10B6500004D02078000704D5112070BD43F2020009 +:10B6600070BD2A4621463046FEF7C9FE18B9286843 +:10B6700060616868A061207840F0080020700020B8 +:10B6800070BD70B50D460646032108F00FFC04009E +:10B6900004D02078000704D4082070BD43F20200D3 +:10B6A00070BD2A4621463046FEF7DDFE00B9A58270 +:10B6B000207820F008002070002070BD2DE9F04FA8 +:10B6C0000E4691B08046032108F0F0FB0446404648 +:10B6D00008F02FFD07460020079008900990ADF86C +:10B6E00030000A9002900390049004B9FFDF0DF13E +:10B6F0000809FFB9FFDF1DE038460BA9002207F05B +:10B7000055FF9DF82C0000F07F050A2D00D3FFDFC8 +:10B710006019017F491E01779DF82C00000609D5AC +:10B720002A460CA907A8FEF7C0FD19F80510491C08 +:10B7300009F80510761EF6B2DED204F13400F84D99 +:10B7400004F1260BDFF8DCA304F12A07069010E0D1 +:10B750005846069900F08CFA064628700A2800D34D +:10B76000FFDF5AF8261040468847E08CC05DB042A3 +:10B7700002D0208D0028EBD10A202870E94D4E46DA +:10B7800028350EE00CA907A800F072FA0446375DD0 +:10B7900055F8240000B9FFDF55F82420394640460B +:10B7A0009047BDF81E000028ECD111B0BDE8F08F25 +:10B7B00010B5032108F07AFB040000D1FFDF0A2254 +:10B7C000002104F11C0002F062FE207840F0040029 +:10B7D000207010BD10B50C46032108F067FB204413 +:10B7E000007F002800D0012010BD2DE9F84F8946C8 +:10B7F00015468246032108F059FB070004D028466D +:10B80000F5F727FD40B903E043F20200BDE8F88FE9 +:10B810004846F5F744FD08B11020F7E7786828B1ED +:10B8200069880089814201D90920EFE7B9F8000051 +:10B830001C2488B100F0A7F980460146384600F084 +:10B84000D1F988B108EB8800796804EBC000085C86 +:10B8500001280BD00820D9E73846FEF79CFC80462B +:10B86000402807D11320D1E70520CFE7FDF7BEFE22 +:10B8700006000BD008EB8800796804EBC0000C18B8 +:10B88000B9F8000020B1E88910B113E01120BDE73C +:10B890002888172802D36888172801D20720B5E71F +:10B8A000686838B12B1D224641463846FFF7E9F853 +:10B8B0000028ABD104F10C0269462046FEF7E1FFF7 +:10B8C000288860826888E082B9F8000030B10220E0 +:10B8D0002070E889A080E889A0B12BE003202070C7 +:10B8E000A889A08078688178402905D180F80280F5 +:10B8F00039465046FEF7D6FD404600F051F9A9F80A +:10B90000000021E07868218B4089884200D90846F0 +:10B910002083A6F802A004203072B9F800007081DC +:10B92000E0897082F181208B3082A08AB08130461C +:10B9300000F017F97868C178402905D180F80380B4 +:10B9400039465046FEF7FFFD00205FE770B50D4613 +:10B950000646032108F0AAFA04000ED0284600F09B +:10B9600012F905460146204600F03CF918B1284678 +:10B9700000F001F920B1052070BD43F2020070BD56 +:10B9800005EB85011C22606802EBC101084400F050 +:10B990003FF908B1082070BD2A462146304600F024 +:10B9A00075F9002070BD2DE9F0410C461746804620 +:10B9B000032108F07BFA0546204600F0E4F804462F +:10B9C00095B10146284600F00DF980B104EB8401E1 +:10B9D0001C22686802EBC1014618304600F018F9D5 +:10B9E00038B10820BDE8F08143F20200FAE70520F3 +:10B9F000F8E73B46324621462846FFF742F8002842 +:10BA0000F0D1E2B229464046FEF75AFF708C083862 +:10BA1000082803D242484078F7F776FA0020E1E799 +:10BA20002DE9F0410D4617468046032108F03EFA05 +:10BA30000446284600F0A7F8064624B13846F5F734 +:10BA400008FC38B902E043F20200CBE73868F5F7AA +:10BA500000FC08B11020C5E73146204600F0C2F8CE +:10BA600060B106EB86011C22606802EBC10145183B +:10BA7000284600F0CDF818B10820B3E70520B1E75B +:10BA8000B888A98A884201D90C20ABE76168E88CA4 +:10BA90004978B0EBC10F01D31320A3E7314620460C +:10BAA00000F094F80146606808234078C20005F170 +:10BAB000240008F082F8D7E90012C0E90012F2B2BF +:10BAC00021464046FEF772FE00208BE72DE9F04745 +:10BAD0000D461F4690468146032108F0E7F90446CB +:10BAE000284600F050F806463CB14DB13846F5F70F +:10BAF000F4FB50B11020BDE8F08743F20200FAE7F2 +:10BB0000606858B1A0F80C8027E03146204600F06C +:10BB100069F818B1304600F02EF828B10520EAE7A0 +:10BB2000300000207861020006EB86011C2260686C +:10BB300002EBC1014518284600F06AF808B1082058 +:10BB4000D9E7A5F80880F2B221464846FEF7B8FECC +:10BB50001FB1A8896989084438800020CBE707F025 +:10BB600084BE017821F00F01491C21F0F001103151 +:10BB70000170FDF73EBD20B94E48807808B1012024 +:10BB80007047002070474B498988884201D10020C6 +:10BB90007047402801D2402000E0403880B2704712 +:10BBA00010B50446402800D9FFDF2046FFF7E3FF29 +:10BBB00010B14048808810BD4034A0B210BD40682C +:10BBC00042690078484302EBC0007047C278406881 +:10BBD000037812FB03F24378406901FB032100EB79 +:10BBE000C1007047C2788A4209D9406801EB8101DF +:10BBF0001C2202EBC101405C08B10120704700200B +:10BC000070470078062801D901207047002070474E +:10BC10000078062801D00120704700207047F0B45A +:10BC200001EB81061C27446807EBC6063444049DDB +:10BC300005262670E3802571F0BCFEF71FBA10B50B +:10BC4000418911B1FFF7DDFF08B1002010BD0120CF +:10BC500010BD10B5C18C8278B1EBC20F04D9C18977 +:10BC600011B1FFF7CEFF08B1002010BD012010BDBB +:10BC700010B50C4601230A22011D07F0D4FF0078FD +:10BC80002188012282409143218010BDF0B402EB53 +:10BC900082051C264C6806EBC505072363554B68D7 +:10BCA0001C79402C03D11A71F0BCFEF791BCF0BC9A +:10BCB000704700003000002010B5EFF3108000F056 +:10BCC000010472B6FC484178491C41704078012853 +:10BCD00001D10BF0B3FA002C00D162B610BD70B5E3 +:10BCE000F54CA07848B90125A570FFF7E5FF0BF0EA +:10BCF000B6FA20B100200BF080FA002070BD4FF0A2 +:10BD00008040E570C0F80453F7E770B5EFF310809A +:10BD100000F0010572B6E84C607800B9FFDF60788A +:10BD2000401E6070607808B90BF08CFA002D00D1CD +:10BD300062B670BDE04810B5817821B10021C170B4 +:10BD40008170FFF7E2FF002010BD10B504460BF034 +:10BD500086FAD9498978084000D001202060002067 +:10BD600010BD10B5FFF7A8FF0BF079FA02220123EE +:10BD7000D149540728B1D1480260236103200872D9 +:10BD800002E00A72C4F804330020887110BD2DE966 +:10BD9000F84FDFF824934278817889F80420002650 +:10BDA00089F80510074689F806600078DFF810B3B7 +:10BDB000354620B1012811D0022811D0FFDF0BF049 +:10BDC00060FA4FF0804498B10BF062FAB0420FD1A4 +:10BDD00030460BF061FA0028FAD042E00126EEE787 +:10BDE000FFF76AFF58460168C907FCD00226E6E75C +:10BDF0000120E060C4F80451B2490E600107D1F897 +:10BE00004412B04AC1F3423124321160AD49343199 +:10BE100008604FF0020AC4F804A3A060AA480168B1 +:10BE2000C94341F3001101F10108016841F010011B +:10BE3000016001E019F0A8FFD4F804010028F9D04E +:10BE400030460BF029FA0028FAD0B8F1000F04D1DF +:10BE50009D48016821F010010160C4F808A3C4F8EE +:10BE6000045199F805004E4680B1387870B90BF04E +:10BE7000F6F980460BF006FC6FF00042B8F1000FB7 +:10BE800002D0C6E9032001E0C6E90302DBF80000A6 +:10BE9000C00701D00BF0DFF9387810B13572BDE87A +:10BEA000F88F4FF01808C4F808830127A7614FF4F2 +:10BEB0002070ADF8000000BFBDF80000411EADF8D5 +:10BEC0000010F9D2C4F80C51C4F810517A48C01DC2 +:10BED0000BF06CFA3570FFF744FF676179493079F0 +:10BEE00020310860C4F80483D9E770B5050000D19B +:10BEF000FFDF4FF080424FF0FF30C2F8080300210F +:10BF0000C2F80011C2F80411C2F80C11C2F81011E5 +:10BF1000694C61700BF0AFF910B10120A070607036 +:10BF200067480068C00701D00BF095F92846BDE8C6 +:10BF300070402CE76048007A002800D0012070474C +:10BF40002DE9F04F61484FF0000A85B0D0F800B0FD +:10BF5000D14657465D4A5E49083211608406D4F8DE +:10BF6000080110B14FF0010801E04FF000080BF09C +:10BF7000F0F978B1D4F8240100B101208246D4F858 +:10BF80001C0100B101208146D4F8200108B101272D +:10BF900000E00027D4F8000100B101200490D4F89B +:10BFA000040100B101200390D4F80C0100B101207C +:10BFB0000290D4F8100100B101203F4D0190287883 +:10BFC00000260090B8F1000F04D0C4F808610120E9 +:10BFD0000BF013F9BAF1000F04D0C4F82461092062 +:10BFE0000BF00BF9B9F1000F04D0C4F81C610A2062 +:10BFF0000BF003F927B1C4F820610B200BF0FDF81A +:10C000002D48C01D0BF0DAF900B1FFDFDFF8AC807E +:10C010000498012780B1C4F80873E87818B1EE706D +:10C0200000200BF0EAF8287A022805D103202872B4 +:10C030000221C8F800102761039808B1C4F8046110 +:10C04000029850B1C4F80C61287A032800D0FFDFB1 +:10C05000C8F800602F72FFF758FE019838B1C4F895 +:10C060001061287A012801D100F05CF8676100981E +:10C0700038B12E70287A012801D1FFF772FEFFF740 +:10C0800044FE0D48C01D0BF0AFF91049091DC1F861 +:10C0900000B005B0BDE8F08F074810B5C01D0BF02B +:10C0A0008DF90549B0B1012008704FF0E021C1F8C9 +:10C0B0000002BDE81040FFE544000020340C0040C1 +:10C0C0000C0400401805004010ED00E0100502408F +:10C0D00001000001087A012801D1FFF742FEBDE806 +:10C0E000104024480BF080B970B5224CE41FA078B2 +:10C0F00008B90BF0A7F801208507A861207A00266F +:10C10000032809D1D5F80C0120B900200BF0C4F8A0 +:10C110000028F7D1C5F80C6126724FF0FF30C5F842 +:10C12000080370BD70B5134CE41F6079F0B10128AD +:10C1300003D0A179401E814218DA0BF090F8054631 +:10C140000BF0A0FA6179012902D9A179491CA171EA +:10C150000DB1216900E0E168411A022902DA11F10A +:10C16000020F06DC0DB1206100E0E060BDE8704028 +:10C17000F7E570BD4B0000200F4A12680D498A4256 +:10C180000CD118470C4A12680A4B9A4206D101B5E5 +:10C190000BF04AFA0BF01DFDBDE8014007490968A4 +:10C1A0000958084706480749054A064B70470000EA +:10C1B00000000000BEBAFECA5C000020040000209F +:10C1C000C8130020C8130020F8B51D46DDE9064756 +:10C1D0000E000AD007F0ADFF2346FF1DBCB231466A +:10C1E0002A46009407F0BBFBF8BDD0192246194639 +:10C1F00002F023F92046F8BD70B50D460446102222 +:10C20000002102F044F9258117206081A07B40F0D5 +:10C210000A00A07370BD4FF6FF720A80014602202B +:10C220000BF04CBC704700897047827BD30701D16B +:10C23000920703D48089088000207047052070474A +:10C24000827B920700D581817047014600200988D2 +:10C2500041F6FE52114200D00120704700B503465E +:10C26000807BC00701D0052000BD59811846FFF72B +:10C27000ECFFC00703D0987B40F004009873987BD4 +:10C2800040F001009873002000BD827B520700D56A +:10C2900009B14089704717207047827B61F3C30260 +:10C2A000827370472DE9FC5F0E460446017896467E +:10C2B000012000FA01F14DF6FF5201EA020962681D +:10C2C0004FF6FF7B1188594502D10920BDE8FC9F3C +:10C2D000B9F1000F05D041F6FE55294201D00120E9 +:10C2E000F4E741EA090111801D0014D000232B70EE +:10C2F00094F800C0052103221F464FF0020ABCF14A +:10C300000E0F76D2DFE80CF0F909252F47646B7722 +:10C31000479193B4D1D80420D8E7616820898B7BFA +:10C320009B0767D517284AD30B89834247D389894E +:10C33000172901D3814242D185F800A0A5F8010058 +:10C340003280616888816068817B21F0020181739D +:10C35000C6E0042028702089A5F801006089A5F8AE +:10C3600003003180BCE0208A3188C01D1FFA80F8AC +:10C37000414524D3062028702089A5F80100608952 +:10C38000A5F80300A089A5F805000721208ACDE9BA +:10C390000001636941E00CF0FF00082810D008207C +:10C3A00028702089A5F801006089A5F80300318074 +:10C3B0006A1D694604F10C0009F025FB10B15EE02E +:10C3C0001020EDE730889DF800100844308087E0A9 +:10C3D0000A2028702089A5F80100328044E00C2052 +:10C3E00028702089A5F801006089A5F80300318034 +:10C3F0003AE082E064E02189338800EB41021FFAD1 +:10C4000082F843453BD3B8F1050F38D30E222A708A +:10C410000BEA4101CDE90010E36860882A467146C5 +:10C42000FFF7D2FEA6F800805AE04020287060890D +:10C430003188C01C1FFA80F8414520D32878714606 +:10C4400020F03F00123028702089A5F80100608993 +:10C45000CDE9000260882A46E368FFF7B5FEA6F83A +:10C460000080287840063BD461682089888037E0C6 +:10C47000A0893288401D1FFA80F8424501D2042766 +:10C480003DE0162028702089A5F801006089A5F8F4 +:10C490000300A089CDE9000160882A46714623691E +:10C4A000FFF792FEA6F80080DEE718202870207AB9 +:10C4B0006870A6F800A013E061680A88920401D4AD +:10C4C00005271CE0C9882289914201D0062716E081 +:10C4D0001E21297030806068018821F4005101809C +:10C4E000B9F1000F0BD061887823002202200BF0F5 +:10C4F0003BFA61682078887006E033800327606823 +:10C50000018821EA090101803846DFE62DE9FF4F65 +:10C5100085B01746129C0D001E461CD03078C1070E +:10C5200003D000F03F00192801D9012100E00021CB +:10C530002046FFF7AAFEA8420DD32088A0F57F4130 +:10C54000FF3908D03078410601D4000605D508200F +:10C5500009B0BDE8F08F0720FAE700208DF8000051 +:10C560008DF8010030786B1E00F03F0C0121A81EF1 +:10C570004FF0050A4FF002094FF0030B9AB2BCF1DD +:10C58000200F75D2DFE80CF08B10745E7468748C29 +:10C59000749C74B574BA74C874D474E1747474F10E +:10C5A00074EF74EE74ED748B052D78D18DF80090D6 +:10C5B000A0788DF804007088ADF8060030798DF809 +:10C5C0000100707800F03F000C2829D00ADCA0F1AF +:10C5D0000200092863D2DFE800F0126215621A62D5 +:10C5E0001D622000122824D004DC0E281BD0102845 +:10C5F000DBD11BE016281FD01828D6D11FE02078E9 +:10C60000800701E020784007002848DAEEE0207833 +:10C610000007F9E72078C006F6E720788006F3E700 +:10C6200020784006F0E720780006EDE72088C00576 +:10C63000EAE720884005E7E720880005E4E720884E +:10C64000C004E1E72078800729D5032D27D18DF894 +:10C6500000B0B6F8010081E0217849071FD5062D0A +:10C660001DD381B27078012803D0022817D102E0CF +:10C67000C9E0022000E0102004228DF8002072782A +:10C680008DF80420801CB1FBF0F2ADF8062092B2C8 +:10C6900042438A4203D10397ADF80890A6E079E0BF +:10C6A0002078000776D598B282088DF800A0ADF802 +:10C6B0000420B0EB820F6DD10297ADF8061095E023 +:10C6C0002178C90666D5022D64D381B206208DF883 +:10C6D0000000707802285DD3B1FBF0F28DF8040001 +:10C6E000ADF8062092B242438A4253D1ADF8089089 +:10C6F0007BE0207880064DD5072003E020784006B7 +:10C700007FD508208DF80000A088ADF80400ADF8B2 +:10C710000620ADF8081068E02078000671D50920E1 +:10C72000ADF804208DF80000ADF8061002975DE02A +:10C730002188C90565D5022D63D381B20A208DF801 +:10C740000000707804285CD3C6E72088400558D5DF +:10C75000012D56D10B208DF80000A088ADF8040003 +:10C7600044E021E026E016E0FFE72088000548D5F8 +:10C77000052D46D30C208DF80000A088ADF80400EC +:10C78000B6F803006D1FADF80850ADF80600ADF81F +:10C790000AA02AE035E02088C00432D5012D30D12E +:10C7A0000D208DF8000021E02088800429D4B6F8FF +:10C7B0000100E080A07B000723D5032D21D3307832 +:10C7C00000F03F001B2818D00F208DF800002088B3 +:10C7D00040F40050A4F80000B6F80100ADF80400E1 +:10C7E000ED1EADF80650ADF808B003976946059800 +:10C7F000F5F792FB050008D016E00E208DF800003A +:10C80000EAE7072510E008250EE0307800F03F0049 +:10C810001B2809D01D2807D0022005990BF04EF9DE +:10C82000208800F400502080A07B400708D52046D7 +:10C83000FFF70BFDC00703D1A07B20F00400A0731D +:10C84000284685E61FB5022806D101208DF8000094 +:10C8500088B26946F5F760FB1FBD0000F8B51D46BC +:10C86000DDE906470E000AD007F063FC2346FF1DF2 +:10C87000BCB231462A46009407F071F8F8BDD019D1 +:10C880002246194601F0D9FD2046F8BD2DE9FF4F9B +:10C890008DB09B46DDE91B57DDF87CA00C46082BCC +:10C8A00005D0E06901F0FEF850B11020D2E02888F0 +:10C8B000092140F0100028808AF80010022617E0B5 +:10C8C000E16901208871E2694FF420519180E169AA +:10C8D0008872E06942F601010181E06900218173FB +:10C8E0002888112140F0200028808AF800100426B2 +:10C8F00038780A900A2038704FF0020904F11800C5 +:10C900004D460C9001F0C6FBB04681E0BBF1100F24 +:10C910000ED1022D0CD0A9EB0800801C80B20221A0 +:10C92000CDE9001005AB52461E990D98FFF796FF12 +:10C93000BDF816101A98814203D9F74800790F9074 +:10C9400004E003D10A9808B138702FE04FF00201DB +:10C95000CDE900190DF1160352461E990D98FFF707 +:10C960007DFF1D980088401B801B83B2C6F1FF002D +:10C97000984200D203461E990BA8D9B15FF000027D +:10C98000DDF878C0CDE9032009EB060189B2CDE9D5 +:10C9900001C10F980090BDF8161000220D9801F00B +:10C9A0000EFC387070B1C0B2832807D0BDF81600F5 +:10C9B00020833AE00AEB09018A19E1E7022011B06D +:10C9C000BDE8F08FBDF82C00811901F0FF08022DA1 +:10C9D0000DD09AF80120424506D1BDF820108142C1 +:10C9E00007D0B8F1FF0F04D09AF801801FE08AF851 +:10C9F0000180C94800680178052902D1BDF81610E8 +:10CA0000818009EB08001FFA80F905EB080085B268 +:10CA1000DDE90C1005AB0F9A01F03FFB28B91D981A +:10CA20000088411B4145BFF671AF022D13D0BBF109 +:10CA3000100F0CD1A9EB0800801C81B20220CDE9B7 +:10CA4000000105AB52461E990D98FFF707FF1D9890 +:10CA50000580002038700020B1E72DE9F8439C469E +:10CA6000089E13460027B26B9AB3491F8CB2F18F10 +:10CA7000A1F57F45FF3D05D05518AD882944891D96 +:10CA80008DB200E000252919B6F83C8008314145F7 +:10CA900020D82A44BCF8011022F8021BBCF803106D +:10CAA00022F8021B984622F8024B914607F02FFB12 +:10CAB0004FF00C0C41464A462346CDF800C006F024 +:10CAC0001AFFF587B16B00202944A41D214408807A +:10CAD00003E001E0092700E083273846BDE8F8833A +:10CAE00010B50B88848F9C420CD9846BE0180488A5 +:10CAF00044B1848824F40044A41D23440B801060B6 +:10CB0000002010BD0A2010BD2DE9F0478AB0002595 +:10CB1000904689468246ADF8185007274BE00598A5 +:10CB200006888088000446D4A8F8006007A801950C +:10CB300000970295CDE903504FF40073002231466F +:10CB4000504601F03CFB04003CD1BDF81800ADF8A4 +:10CB50002000059804888188B44216D10A0414D4B0 +:10CB600001950295039521F400410097049541F445 +:10CB7000804342882146504601F0BFF804000BD1A3 +:10CB80000598818841F40041818005AA08A948469A +:10CB9000FFF7A6FF0400DCD00097059802950195E9 +:10CBA000039504950188BDF81C300022504601F021 +:10CBB000A4F80A2C06D105AA06A94846FFF790FF5B +:10CBC0000400ACD0ADF8185004E00598818821F439 +:10CBD0000041818005AA06A94846FFF781FF002889 +:10CBE000F3D00A2C03D020460AB0BDE8F08700201D +:10CBF000FAE710B50C46896B86B051B10C218DF85F +:10CC00000010A18FADF80810A16B01916946FAF7E9 +:10CC100001FB00204FF6FF71A063E187A08706B0FB +:10CC200010BD2DE9F0410D460746896B0020069E98 +:10CC30001446002911D0012B0FD13246294638461F +:10CC4000FFF762FF002808D1002C06D032462946A3 +:10CC50003846BDE8F04100F034BFBDE8F0812DE971 +:10CC6000FC411446DDE9087C0E46DDE90A15521D3B +:10CC7000BCF800E092B2964502D20720BDE8FC81E4 +:10CC8000ACF8002017222A70A5F80160A5F803303F +:10CC90000522CDE900423B462A46FFF7DFFD002092 +:10CCA000ECE770B50C46154648220021204601F0FD +:10CCB000EEFB04F1080044F81C0F00204FF6FF7152 +:10CCC000E06161842084A5841720E08494F82A0020 +:10CCD00040F00A0084F82A0070BD4FF6FF720A8007 +:10CCE000014603200AF0EABE30B585B00C46054681 +:10CCF000FFF77FFFA18E284629B101218DF8001092 +:10CD00006946FAF787FA0020E0622063606305B0A5 +:10CD100030BDB0F8400070476000002090F8462019 +:10CD2000920703D4408808800020F4E70620F2E749 +:10CD300090F846209207EED5A0F84410EBE70146A4 +:10CD4000002009880A0700D5012011F0F00F01D05A +:10CD500040F00200CA0501D540F004008A0501D563 +:10CD600040F008004A0501D540F010000905D2D571 +:10CD700040F02000CFE700B5034690F84600C0071A +:10CD800001D0062000BDA3F842101846FFF7D7FFD8 +:10CD900010F03E0F05D093F8460040F0040083F8F1 +:10CDA000460013F8460F40F001001870002000BD47 +:10CDB00090F84620520700D511B1B0F84200AAE71A +:10CDC0001720A8E710F8462F61F3C3020270A2E70C +:10CDD0002DE9FF4F9BB00E00DDE92B34DDE929780A +:10CDE000289D24D02878C10703D000F03F001928DF +:10CDF00001D9012100E000212046FFF7D9FFB04210 +:10CE000015D32878410600F03F010CD41E290CD020 +:10CE1000218811F47F6F0AD13A8842B1A1F57F428F +:10CE2000FF3A04D001E0122901D1000602D5042006 +:10CE30001FB0C5E5FA491D984FF0000A08718DF83A +:10CE400018A08DF83CA00FAA0A60ADF81CA0ADF8A0 +:10CE500050A02978994601F03F02701F5B1C04F135 +:10CE6000180C4FF0060E4FF0040BCDF858C01F2AD7 +:10CE70007ED2DFE802F07D7D107D267DAC7DF47DE5 +:10CE8000F37DF27DF17DF47DF07D7D7DEF7DEE7DA6 +:10CE90007D7D7D7DED0094F84610B5F80100890791 +:10CEA00001D5032E02D08DF818B01EE34FF40061B7 +:10CEB000ADF85010608003218DF83C10ADF84000B3 +:10CEC000D4E2052EEFD1B5F801002083ADF81C00A7 +:10CED000B5F80310618308B1884201D9012079E1D6 +:10CEE0000020A07220814FF6FF702084169801F078 +:10CEF000D1F8052089F800000220029083460AAB91 +:10CF00001D9A16991B9801F0C8F890BB9DF82E0049 +:10CF1000012804D0022089F80100102003E001203C +:10CF200089F8010002200590002203A90BA808F04F +:10CF30006AFDE8BB9DF80C00059981423DD1398816 +:10CF4000801CA1EB0B01814237DB02990220CDE965 +:10CF500000010DF12A034A4641461B98FFF77EFC6B +:10CF600002980BF1020B801C81B217AA029101E01A +:10CF70009CE228E003A90BA808F045FD02999DF862 +:10CF80000C00CDE9000117AB4A4641461B98FFF75C +:10CF900065FC9DF80C000AAB0BEB00011FFA81FB4E +:10CFA00002991D9A084480B2029016991B9800E0DD +:10CFB00003E001F072F80028B6D0BBF1020F02D0F6 +:10CFC000A7F800B04FE20A208DF818004BE20021CC +:10CFD0000391072EFFF467AFB5F801002083ADF889 +:10CFE0001C00B5F80320628300283FF477AF90421D +:10CFF0003FF674AF0120A072B5F805002081002033 +:10D00000A073E06900F04EFD78B9E16901208871F4 +:10D01000E2694FF420519180E1698872E16942F63A +:10D0200001000881E06900218173F01F20841E98AF +:10D03000606207206084169801F02CF8072089F8B8 +:10D0400000000120049002900020ADF82A0028E0A2 +:10D0500019E29FE135E1E5E012E2A8E080E043E07B +:10D060000298012814D0E0698079012803D1BDF825 +:10D070002800ADF80E00049803ABCDE900B04A4695 +:10D0800041461B98FFF7EAFB0498001D80B204900C +:10D09000BDF82A00ADF80C00ADF80E00059880B27E +:10D0A00002900AAB1D9A16991B9800F0F6FF28B95A +:10D0B00002983988001D05908142D1D2029801283A +:10D0C00081D0E0698079012803D1BDF82800ADF84E +:10D0D0000E00049803ABCDE900B04A4641461B98C8 +:10D0E000FFF7BCFB0298BDE1072E02D0152E7FF49E +:10D0F000DAAEB5F801102183ADF81C10B5F80320A5 +:10D10000628300293FF4EAAE91423FF6E7AE012187 +:10D11000A1724FF0000BA4F808B084F80EB0052EF1 +:10D1200007D0C0B2691DE26908F06BFC00287FF4EB +:10D130004AAF4FF6FF70208401A906AA14A8CDF8C3 +:10D1400000B081E885032878214600F03F031D9A4E +:10D150001B98FFF79BFB8246208BADF81C0082E1F9 +:10D160000120032EC3D14021ADF85010B5F80110B5 +:10D170002183ADF81C100AAAB8F1000F00D00023DB +:10D18000CDE9020304921D98CDF804800090388800 +:10D190000022401E83B21B9801F011F88DF8180090 +:10D1A00090BB0B2089F80000BDF8280035E04FF057 +:10D1B000010C052E9BD18020ADF85000B5F8011070 +:10D1C0002183B5F803002084ADF81C10B0F5007F72 +:10D1D00003D907208DF8180087E140F47C422284AF +:10D1E0000CA8B8F1000F00D00023CDE90330CDE941 +:10D1F000018C1D9800903888401E83B21B9800F067 +:10D20000DEFF8DF8180018B18328A8D10220BFE0F6 +:10D210000D2189F80010BDF83000401C22E100000B +:10D2200060000020032E04D248067FF53CAE0020AB +:10D2300018E1B5F80110ADF81C102878400602D5A9 +:10D240008DF83CE002E007208DF83C004FF000082C +:10D250000320CDE902081E9BCDF810801D98019394 +:10D26000A6F1030B00901FFA8BF342461B9800F0C7 +:10D2700044FD8DF818008DF83C80297849060DD5BD +:10D280002088C00506D5208BBDF81C10884201D12E +:10D29000C4F8248040468DF81880E3E0832801D14B +:10D2A0004FF0020A4FF48070ADF85000BDF81C003A +:10D2B0002083A4F820B01E986062032060841321AC +:10D2C000CDE0052EFFF4EFADB5F80110ADF81C1060 +:10D2D000A28F6AB3A2F57F43FE3B29D008228DF8C6 +:10D2E0003C2000BF4FF0000B0523CDE9023BDDF8E9 +:10D2F00078C0CDF810B01D9A80B2CDF804C040F4CB +:10D3000000430092B5F803201B9800F0F6FC8DF85E +:10D310003CB04FF400718DF81800ADF85010832820 +:10D3200010D0F8B1A18FA1F57F40FE3807D0DCE026 +:10D330000B228DF83C204FF6FE72A287D2E7A4F8AC +:10D340003CB0D2E000942B4631461E9A1B98FFF762 +:10D3500084FB8DF8180008B183284BD1BDF81C0060 +:10D36000208353E700942B4631461E9A1B98FFF703 +:10D3700074FB8DF81800E8BBE18FA06B0844831D97 +:10D380008DE888034388828801881B98FFF767FC33 +:10D39000824668E095F80180022E70D15FEA0800AD +:10D3A00002D0B8F1010F6AD109208DF83C0007A81E +:10D3B00000908DF840804346002221461B98FFF7DD +:10D3C00030FC8DF842004FF0000B8DF843B050B99F +:10D3D000B8F1010F12D0B8F1000F04D1A18FA1F55F +:10D3E0007F40FF380AD0A08F40B18DF83CB04FF499 +:10D3F000806000E037E0ADF850000DE00FA91B9809 +:10D40000F9F708FF82468DF83CB04FF48060ADF824 +:10D410005000BAF1020F06D0FC480068C07928B16C +:10D420008DF8180027E0A4F8188044E0BAF1000F46 +:10D4300003D081208DF818003DE007A800904346F6 +:10D44000012221461B98FFF7ECFB8DF818002146BE +:10D450001B98FFF7CEFB9DF8180020B9192189F819 +:10D460000010012038809DF83C0020B10FA91B98C6 +:10D47000F9F7D0FE8246BAF1000F33D01BE018E076 +:10D480008DF818E031E02078000712D5012E10D178 +:10D490000A208DF83C00E088ADF8400003201B997D +:10D4A0000AF00CFB0820ADF85000C0E648067FF5F6 +:10D4B000FAAC4FF0040A2088BDF8501008432080D1 +:10D4C000BDF8500080050BD5A18FA1F57F40FE3837 +:10D4D00006D11E98E06228982063A6864FF0030AC2 +:10D4E0005046A5E49DF8180078B1012089F80000A5 +:10D4F000297889F80110BDF81C10A9F802109DF8D0 +:10D50000181089F80410052038802088BDF85010C4 +:10D5100088432080E4E72DE9FF4F8846087895B0DE +:10D52000012181404FF20900249C0140ADF82010F8 +:10D530002088DDF88890A0F57F424FF0000AFF3A7E +:10D5400006D039B1000705D5012019B0BDE8F08F2C +:10D550000820FAE7239E4FF0000B0EA886F800B0D3 +:10D5600018995D460988ADF83410A8498DF81CB0AB +:10D57000179A0A718DF838B0086098F800000128F1 +:10D580003BD0022809D003286FD1307820F03F002B +:10D590001D303070B8F80400E08098F800100320C7 +:10D5A000022904D1317821F03F011B31317094F808 +:10D5B0004610090759D505ABB9F1000F13D000216A +:10D5C00002AA82E80B000720CDE90009BDF834006B +:10D5D000B8F80410C01E83B20022159800F0EFFDC9 +:10D5E0000028D1D101E0F11CEAE7B8F80400A6F860 +:10D5F0000100BDF81400C01C04E198F805108DF876 +:10D600001C1098F80400012806D04FF4007A022874 +:10D610002CD00328B8D16CE12188B8F8080011F4A7 +:10D620000061ADF8201020D017281CD3B4F84010AA +:10D63000814218D3B4F84410172901D3814212D182 +:10D64000317821F03F01C91C3170A6F80100032197 +:10D65000ADF83410A4F8440094F8460020F002001D +:10D6600084F8460065E105257EE177E1208808F130 +:10D67000080700F4FE60ADF8200010F0F00F1BD09A +:10D6800010F0C00F03D03888228B9042EBD199B9AB +:10D69000B878C00710D0B9680720CDE902B1CDF83D +:10D6A00004B00090CDF810B0FB88BA88398815987E +:10D6B00000F023FB0028D6D12398BDF82010401C91 +:10D6C00080294ED006DC10290DD020290BD040290E +:10D6D00087D124E0B1F5807F6ED051457ED0B1F581 +:10D6E000806F97D1DEE0C80601D5082000E0102049 +:10D6F00082460DA907AA0520CDE902218DF8380040 +:10D70000ADF83CB0CDE9049608A93888CDE9000110 +:10D710005346072221461598FFF7B8F8A8E09DF870 +:10D720001C2001214FF00A0A002A9BD105ABB9F158 +:10D73000000F00D00020CDE902100720CDE900093C +:10D74000BDF834000493401E83B2218B002215984B +:10D7500000F035FD8DF81C000B203070BDF8140072 +:10D7600020E09DF81C2001214FF00C0A002A22D154 +:10D7700013ABB9F1000F00D00020CDE90210072053 +:10D78000CDE900090493BDF83400228C401E83B219 +:10D79000218B159800F013FD8DF81C000D203070C2 +:10D7A000BDF84C00401CADF8340005208DF8380061 +:10D7B000208BADF83C00BCE03888218B88427FF498 +:10D7C00052AF9DF81C004FF0120A00281CD1606A6D +:10D7D000A8B1B878C0073FF446AF00E018E0BA68D7 +:10D7E0000720CDE902B2CDF804B00090CDF810B01A +:10D7F000FB88BA88159800F080FA8DF81C00132079 +:10D8000030700120ADF8340093E00000600000208B +:10D810003988208B8142D2D19DF81C004FF0160A26 +:10D820000028A06B08D0E0B34FF6FF7000215F46E0 +:10D83000ADF808B0019027E068B1B978C907BED14A +:10D84000E18F0DAB0844821D03968DE80C024388DE +:10D850008288018809E0B878C007BCD0BA680DABEF +:10D8600003968DE80C02BB88FA881598FFF7F7F944 +:10D8700005005ED0072D72D076E0019005AA02A9BE +:10D880002046FFF72DF90146E28FBDF808008242DD +:10D8900001D00029F1D0E08FA16B084407800198E6 +:10D8A000E08746E09DF81C004FF0180A40B1208B3D +:10D8B000C8B13888208321461598FFF79AF938E0D7 +:10D8C00004F118000090237E012221461598FFF7ED +:10D8D000A8F98DF81C000028EDD119203070012026 +:10D8E000ADF83400E7E7052521461598FFF781F9E3 +:10D8F0003AE0208800F40070ADF8200050452DD1AA +:10D90000A08FA0F57F41FE3901D006252CE0D8F884 +:10D9100008004FF0160A48B1A063B8F80C10A187B0 +:10D920004FF6FF71E187A0F800B002E04FF6FF70FC +:10D93000A087BDF8200030F47F611AD07823002240 +:10D94000032015990AF010F898F80000207120883B +:10D95000BDF82010084320800EE000E00725208855 +:10D96000BDF8201088432080208810F47F6F1CD0E1 +:10D970003AE02188814321809DF8380020B10EA92A +:10D980001598F9F747FC05469DF81C000028EBD0D8 +:10D9900086F801A001203070208B70809DF81C005B +:10D9A00030710520ADF83400DEE7A18EE1B11898A2 +:10D9B0000DAB0088ADF834002398CDE90304CDE920 +:10D9C0000139206B0090E36A179A1598FFF700FA67 +:10D9D000054601208DF838000EA91598F9F71AFCB4 +:10D9E00000B10546A4F834B094F8460040070AD5C3 +:10D9F0002046FFF7A4F910F03E0F04D114F8460FAB +:10DA000020F0040020701898BDF8341001802846DA +:10DA10009BE500B585B0032806D102208DF80000F3 +:10DA200088B26946F9F7F6FB05B000BD10B5384C71 +:10DA30000B782268012B02D0022B2AD111E0137837 +:10DA40000BB1052B01D10423137023688A889A80B7 +:10DA50002268CB88D38022680B8913814989518140 +:10DA60000DE08B8893802268CB88D38022680B8955 +:10DA700013814B8953818B899381096911612168D5 +:10DA8000F9F7C8FB226800210228117003D0002892 +:10DA900000D0812010BD832010BD806B002800D0F5 +:10DAA000012070478178012909D10088B0F5205FF5 +:10DAB00003D042F60101884201D1002070470720BF +:10DAC0007047F0B587B0002415460E460746ADF8FE +:10DAD000184011E005980088288005980194811D60 +:10DAE000CDE902410721049400918388428801888E +:10DAF000384600F002F930B905AA06A93046FEF70B +:10DB0000EFFF0028E6D00A2800D1002007B0F0BDC2 +:10DB10006000002010B58B7883B102789A4205D15D +:10DB20000B885BB102E08B79091D4BB18B789A426F +:10DB3000F9D1B0F801300C88A342F4D1002010BD17 +:10DB4000812010BD072826D012B1012A27D103E079 +:10DB5000497801F0070102E04978C1F3C2010529C3 +:10DB60001DD2DFE801F00318080C12000AB10320EF +:10DB700070470220704704280DD250B10DE00528EF +:10DB800009D2801E022808D303E0062803D0032808 +:10DB900003D005207047002070470F207047812078 +:10DBA0007047C0B282060BD4000607D5FA48807AC7 +:10DBB0004143C01D01EBD00080B27047084670475A +:10DBC0000020704770B513880B800B781C0625D594 +:10DBD000F14CA47A844204D843F01000087000206D +:10DBE00070BD956800F0070605EBD0052D78F5406F +:10DBF00065F304130B701378D17803F0030341EA43 +:10DC0000032140F20123B1FBF3F503FB15119268E8 +:10DC1000E41D00FB012000EBD40070BD906870BDD6 +:10DC200037B51446BDF804101180117841F0040195 +:10DC300011709DF804100A061ED5D74AA368C1F3D7 +:10DC40000011927A824208D8FE2811D1D21DD20842 +:10DC50004942184600F01BFC0AE003EBD00200F03A +:10DC60000703012510789D40A84399400843107090 +:10DC7000207820F0100020703EBD2DE9F0410746CD +:10DC8000C81C0E4620F00300B04202D08620BDE83A +:10DC9000F081C14D002034462E60AF802881AA72E9 +:10DCA000E8801AE0E988491CE980810614D4E1780B +:10DCB00000F0030041EA002040F20121B0FBF1F244 +:10DCC00001FB12012068FFF76CFF2989084480B22C +:10DCD0002881381A3044A0600C3420784107E1D400 +:10DCE0000020D4E7AC4801220189C08800EB400045 +:10DCF00002EB8000084480B270472DE9FF4F89B0E5 +:10DD00001646DDE9168A0F46994623F44045084633 +:10DD100000F054FB040002D02078400703D4012017 +:10DD20000DB0BDE8F08F099806F086F802902078D3 +:10DD3000000606D59848817A0298814201D887204A +:10DD4000EEE7224601A90298FFF73CFF8346002038 +:10DD50008DF80C004046B8F1070F1AD00122214679 +:10DD6000FFF7F0FE0028DBD12078400611D5022015 +:10DD70008DF80C00ADF81070BDF80400ADF812007D +:10DD8000ADF814601898ADF81650CDF81CA0ADF899 +:10DD900018005FEA094004D500252E46A846012751 +:10DDA0000CE02178E07801F0030140EA012040F224 +:10DDB0000121B0FBF1F2804601FB12875FEA494086 +:10DDC00009D5B84507D1A178207901F0030140EACF +:10DDD0000120B04201D3BE4201D90720A0E7A81913 +:10DDE0001FFA80F9B94501D90D2099E79DF80C007B +:10DDF00028B103A90998F9F70BFA002890D1B84582 +:10DE000007D1A0784FEA192161F30100A07084F8CE +:10DE100004901A9800B10580199850EA0A0027D09A +:10DE2000199830B10BEB06002A46199900F005FB52 +:10DE30000EE00BEB06085746189E099806F067F9A6 +:10DE40002B46F61DB5B239464246009505F053FD06 +:10DE5000224601A90298FFF7B5FE9DF8040022466C +:10DE600020F010008DF80400DDE90110FFF7D8FE66 +:10DE7000002055E72DE9FF4FDFF81C91824685B061 +:10DE8000B9F80610D9F8000001EB410100EB81045C +:10DE900040F20120B2FBF0F1174600FB1175DDE9FD +:10DEA000138B4E4629460698FFF77BFE0346FFF785 +:10DEB00019FF1844B1880C30884202D9842009B077 +:10DEC0002FE70698C6B2300603D5B00601D5062066 +:10DED000F5E7B9F80620521C92B2A9F80620BBF16A +:10DEE000000F01D0ABF80020B00602D5C4F80880BE +:10DEF0000AE0B9F808201A4492B2A9F80820D9F823 +:10DF00000000891A0844A0602246FE200699FFF707 +:10DF100087FEE77025712078390A61F301002A0A2B +:10DF2000A17840F0040062F30101A17020709AF81A +:10DF300002006071BAF80000E08000252573300609 +:10DF400002D599F80A7000E00127B00601D54FF01C +:10DF500000084E4600244FF007090FE0CDE90258B3 +:10DF60000195CDF800900495F1882046129B089AFF +:10DF7000FFF7C3FE0028A2D1641CE4B2BC42EDD37B +:10DF800000209CE700B5FFF7ADFE03490C308A88FE +:10DF9000904203D9842000BD00060020CA8808688A +:10DFA00002EB420300EB8300521C037823F00403CE +:10DFB0000370CA80002101730846ECE72DE9F047A1 +:10DFC000804600F0FBF9070005D000264446F74DD7 +:10DFD00040F2012916E00120BDE8F087204600F05C +:10DFE000EDF90278C17802F0030241EA0222B2FBA5 +:10DFF000F9F309FB13210068FFF7D3FD3044641CDB +:10E0000086B2A4B2E988601E8142E7DCA8F1010073 +:10E01000E8802889801B288100203870DCE710B553 +:10E02000144631B1491E218005F006FFA070002082 +:10E0300010BD012010BD70B50446DC48C1880368DE +:10E0400001E0401C20802088884207D200EB40027B +:10E0500013EB820202D015786D07F2D580B28842A8 +:10E0600016D2AAB15079A072D08820819178107907 +:10E0700001F0030140EA0120A081A078E11CFFF734 +:10E08000A1FD20612088401C2080E080002070BD20 +:10E090000A2070BD0121018270472DE9FF4F85B034 +:10E0A0004FF6FF798246A3F8009048681E460D4659 +:10E0B00080788DF8060048680088ADF804000020DC +:10E0C0008DF80A00088A0C88A04200D304462C82EE +:10E0D00051E03878400708D4641C288AA4B2401C58 +:10E0E000288208F10100C0B246E0288A401C28823C +:10E0F000781D6968FFF70EFDD8BB3188494501D10D +:10E10000601E30803188A1EB080030806888A04212 +:10E1100038D3B878397900F0030041EA002801A922 +:10E12000781DFFF7F7FC20BB298949452ED0002236 +:10E1300039460798FFF706FDD8B92989414518D116 +:10E14000E9680391B5F80AC0D7F808B05046CDF891 +:10E1500000C005F0DCFFDDF800C05A460CF1070CEA +:10E160001FFA8CFC43460399CDF800C005F08DFBE7 +:10E1700060B1641CA4B200208046204600F01EF965 +:10E180000700A6D1641E2C820A2098E67480787954 +:10E19000B071F888B0803978F87801F0030140EA6E +:10E1A00001207081A6F80C80504605F045FE3A46E5 +:10E1B00006F10801FFF706FD306100207FE62DE93A +:10E1C000FF4F87B081461C469246DDF860B0DDF80F +:10E1D0005480089800F0F2F8050002D02878400733 +:10E1E00002D401200BB09CE5484605F025FE2978B5 +:10E1F000090605D56D49897A814201D88720F1E762 +:10E20000CAF309062A4601A9FFF7DCFC0746149861 +:10E2100007281CD000222946FFF794FC0028E1D1F2 +:10E220002878400613D501208DF808000898ADF82D +:10E230000C00BDF80400ADF80E00ADF81060ADF8AC +:10E24000124002A94846F8F7E3FF0028CAD129780E +:10E25000E87801F0030140EA0121AA78287902F068 +:10E26000030240EA0220564507D0B1F5007F04D9E9 +:10E27000611E814201DD0B20B4E7864201D90720EF +:10E28000B0E7801B85B2A54200D92546BBF1000F3F +:10E2900001D0ABF80050179818B1B9192A4600F010 +:10E2A000CCF8B8F1000F0DD03E4448464446169FC6 +:10E2B00005F03FFF2146FF1DBCB232462B460094BD +:10E2C00005F04DFB00208DE72DE9F04107461D4686 +:10E2D0001646084600F072F8040002D02078400785 +:10E2E00001D40120D3E4384605F0A6FD21780906C3 +:10E2F00005D52E49897A814201D88720C7E4224674 +:10E300003146FFF75FFC65B12178E07801F0030149 +:10E3100040EA0120B0F5007F01D8012000E0002094 +:10E3200028700020B3E42DE9F04107461D4616464B +:10E33000084600F043F8040002D02078400701D4DA +:10E340000120A4E4384605F077FD2178090605D5BB +:10E350001649897A814201D8872098E422463146BD +:10E36000FFF75EFCFF2D14D02178E07801F0030266 +:10E3700040EA022040F20122B0FBF2F302FB13005C +:10E3800015B900F2012080B2E070000A60F30101CB +:10E39000217000207BE410B50C4600F00FF810B19E +:10E3A0000178490704D4012010BD000000060020B8 +:10E3B000C18821804079A0700020F5E70749CA880C +:10E3C000824209D340B1096800EB40006FF00B02B4 +:10E3D00002EB8000084470470020704700060020D0 +:10E3E00070B504460D4621462B460AB9002070BD83 +:10E3F00001E0491C5B1C501E021E03D008781E78E9 +:10E40000B042F6D008781E78801BF0E730B50C4695 +:10E4100001462346051B954206D202E0521E9D5C32 +:10E420008D54002AFAD107E004E01D780D70491CD4 +:10E430005B1C521E002AF8D130BDF0B50E460146D5 +:10E44000334680EA030404F00304B4B906E002B9D9 +:10E45000F0BD13F8017B01F8017B521E01F00307A8 +:10E46000002FF4D10C461D4602E080CD80C4121F5F +:10E47000042AFAD221462B4600BF04E013F8014BD0 +:10E4800001F8014B521E002AF8D100BFE0E7F0B5B9 +:10E490000C460146E6B204E002B9F0BD01F8016B9A +:10E4A000521E01F00307002FF6D10B46E5B245EAF4 +:10E4B000052545EA054501E020C3121F042AFBD2C9 +:10E4C000194602E001F8016B521E002AFAD100BF82 +:10E4D000E3E7000010B509F0A0FDF4F7F9F909F041 +:10E4E000E7FBBDE8104009F0AFBC302834BF012085 +:10E4F00000207047202834BF4FF0A0420C4A01236F +:10E5000000F01F0003FA00F0002914BFC2F80C0548 +:10E51000C2F808057047202834BF4FF0A0410449D5 +:10E5200000F01F00012202FA00F0C1F81805704740 +:10E530000003005070B50346002002466FF02F051F +:10E540000EE09C5CA4F130060A2E02D34FF0FF309F +:10E5500070BD00EB800005EB4000521C2044D2B29D +:10E560008A42EED370BD30B50A230BE0B0FBF3F462 +:10E5700003FB1404B0FBF3F08D183034521E05F881 +:10E58000014CD2B2002AF1D130BD30B500234FF694 +:10E59000FF7510E0040A44EA002084B2C85C6040C1 +:10E5A000C0F30314604005EA00344440E0B25B1C51 +:10E5B00084EA40109BB29342ECD330BD2DE9F04188 +:10E5C000FE4B0026012793F864501C7893F868C02E +:10E5D000B8B183F89140A3F8921083F8902083F8A3 +:10E5E0008E70BCF1000F0CBF83F8946083F89450D8 +:10E5F000F3488068008805F08AFDBDE8F04105F029 +:10E6000021BA4FF6FF7083F89140A3F8920083F887 +:10E61000902083F88E70BCF1000F14BF83F89450E3 +:10E6200083F89460BDE8F0812DE9F041E44D29685C +:10E6300091F89C200024012A23D091F89620012AE9 +:10E6400030D091F86C301422DC4E0127012B32D0EF +:10E6500091F88E30012B4FD091F8A620012A1CBFD3 +:10E660000020BDE8F08144701F2200F8042B222214 +:10E67000A731FFF7E2FE286880F8A6400120BDE838 +:10E68000F08144701B220270D1F89D204260D1F8C5 +:10E69000A120826091F8A520027381F89C4001209E +:10E6A000BDE8F081447007220270D1F898204260E2 +:10E6B00081F89640E2E78046447000F8042B20225F +:10E6C0006E31FFF7BAFE88F80870286880F86C4051 +:10E6D00090F86E000028D1D1B6F87000A6F8980026 +:10E6E000A868417B86F89A1086F89670008805F035 +:10E6F0000EFD05F0B6F9C1E791F86C30012B0BD097 +:10E70000447017220270D1F890204260B1F8942032 +:10E71000028181F88E40B1E78046447000F8042BF6 +:10E7200020226E31FFF789FE88F80870286880F88B +:10E730006C4090F86E000028A0D1CDE7A04800689A +:10E7400090F86C10002914BFB0F870004FF6FF70FD +:10E75000704770B59A4C06462068002808BFFFDF56 +:10E760000025206845706660002808BFFFDF20682C +:10E77000417800291CBFFFDF70BDCC220021FFF7CC +:10E7800086FE2068FF2101707F2180F83810132158 +:10E790004184282180F86910012180F85C1080F8FC +:10E7A00061500AF0C1F9BDE8704009F0AEBA844981 +:10E7B0000968097881420CBF012000207047804819 +:10E7C000006890F82200C0F3400070477C48006861 +:10E7D00090F8220000F0010070477948006890F836 +:10E7E0002200C0F3001070472DE9F0437448002464 +:10E7F000036893F82400B3F822C0C0F38001C0F38B +:10E800004002114400F001000844CCF3001121B390 +:10E81000BCF1100F02BF6B4931F81000BDE8F08366 +:10E82000BCF1120F18BFBCF1130F0ED0BCF1150FC5 +:10E830001EBFFFDF2046BDE8F0830021624A32F8A8 +:10E84000102010FB0120BDE8F083604A002132F85F +:10E85000102010FB0120BDE8F08393F85E2093F8B0 +:10E860005F102E264FF47A774FF014084FF04009CE +:10E87000022A04BF4AF2D745B5FBF7F510D0012AAA +:10E8800004BF4AF22F75B5FBF7F510D04AF62315F1 +:10E89000B5FBF7F5082A08BF4E4613D0042A18D056 +:10E8A0002646082A0ED0042A13D0022A49D004F1A1 +:10E8B0002806042A0FD0082A1CBF4FF01908082286 +:10E8C00004D00AE04FF0140806F5A8764FF0400295 +:10E8D00003E006F5A8764FF0100218FB026212FB67 +:10E8E0000052C0EB00103A4D00EB800005EB8000B9 +:10E8F00010441CF0010F4FF4C8724FF4BF7504BFF1 +:10E90000CCF34006002E65D0CCF3400600F5A57090 +:10E91000EEB1082904BF174640260CD0042904BFD5 +:10E920002F46102607D0022907BF04F11807042636 +:10E9300004F12807082606EB860808EB86163E44F5 +:10E940001BE004F118064FF019080422C5E7082956 +:10E9500004BF164640270CD0042904BF2E461027BA +:10E9600007D0022907BF04F11806042704F128067E +:10E97000082707EB871706EB8706304400F19C0653 +:10E9800093F8690001F00C07002F08BF0020304405 +:10E9900018BF00F5416027D1082904BF164640275B +:10E9A0001BD0042904BF2E46102716D0022906BF0B +:10E9B00004F11806042704F128060CE00C060020D8 +:10E9C00068000020DC610200E4610200D461020002 +:10E9D000D4FEFFFF64E018BF0827C7EBC70707EBAB +:10E9E000470706EB4706304498301CF0010F17D05C +:10E9F000082908BF40210CD0042904BF2A46102151 +:10EA000007D0022907BF04F11802042104F12802EB +:10EA1000082101EB410303EB0111114408443BE0E1 +:10EA2000082904BF944640260CD0042904BFAC46F4 +:10EA3000102607D0022907BF04F1180C042604F1A0 +:10EA4000280C082606EB8616B3F840300CEB860C33 +:10EA50006044EB2B20D944F2552C0B3303FB0CF311 +:10EA60009B0D082907D0042902D0022905D008E00F +:10EA70002A46102108E0402106E004F11802042192 +:10EA800002E004F12802082101EB811102EB81016F +:10EA900001F5A57103FB010000F5B470BDE8F0833A +:10EAA00000F5A570082904BF944640260CD004291F +:10EAB00004BFAC46102607D0022907BF04F1180C8A +:10EAC000042604F1280C082606EB8616B3F8483015 +:10EAD0000CEB860C6044EB2BDED944F2552C0B3347 +:10EAE00003FB0CF39B0D0829C5D00429C0D00229D3 +:10EAF000C7D1C2E7FE4840F271210068806A4843EE +:10EB00007047FB48006890F83700002818BF0120C4 +:10EB1000704710B5F74C207B022818BF032808D196 +:10EB2000207D04F115010EF0E6FE08281CBF01202F +:10EB300010BD207B002816BF022800200120BDE860 +:10EB400010400AF021BDEB4908737047E849096895 +:10EB500081F8300070472DE9F047E54C2168087BCB +:10EB6000002816BF022800200120487301F10E0181 +:10EB70000AF0F4FC2168087B022816BF0328012252 +:10EB8000002281F82F204FF0080081F82D00487BEB +:10EB900001F10E034FF001064FF00007012804BFFA +:10EBA0005B7913F0C00F0AD001F10E03012804D1E4 +:10EBB000587900F0C000402801D0002000E001207A +:10EBC00081F82E00002A04BF91F8220010F0040FF3 +:10EBD00007D0087D01F115010EF08DFE216881F846 +:10EBE0002D002068476007F0BFFA2168C14D4FF043 +:10EBF0000009886095F82D000EF089FE804695F892 +:10EC00002F00002818BFB8F1000F04D095F82D0090 +:10EC10000EF0B1FC68B195F8300000281CBF95F8E3 +:10EC20002E0000281DD0697B05F10E0001290ED0B1 +:10EC300012E06E734A4605F10E0140460AF0E4FC0C +:10EC400095F82D1005F10E000EF063FF09E04079F4 +:10EC500000F0C000402831D0394605F10E000AF01E +:10EC60000BFD2068C77690F8220010F0040F08BF53 +:10EC7000BDE8F087002795F82D000EF017FD050080 +:10EC800008BFBDE8F087102102F0C2F8002818BFC5 +:10EC9000BDE8F08720683A4600F11C01C676284698 +:10ECA0000AF0B2FC206800F11C0160680FF08EF8D9 +:10ECB0006068BDE8F04701210FF0A3B80EF066FFD1 +:10ECC0004A4605F10E010AF09FFCCAE7884A12681D +:10ECD000137B0370D2F80E000860508A888070475A +:10ECE00078B584490446824E407B087332682078A8 +:10ECF00010706088ADF8000080B200F00101C0F330 +:10ED0000400341EA4301C0F3800341EA8301C0F3B9 +:10ED1000C00341EAC301C0F3001341EA0311C0F389 +:10ED2000401341EA4311C0F3801041EA801050843F +:10ED3000E07D012808BF012507D0022808BF022571 +:10ED400003D0032814BFFFDF0825306880F85E5029 +:10ED5000607E012808BF012507D0022808BF0225D0 +:10ED600003D0032814BFFFDF0825316881F85F5006 +:10ED700091F83500012829D0207B81F82400488CA7 +:10ED80001D280CBF002060688862607D81F8370014 +:10ED9000A07B002816BF0228002001200875D4F8A7 +:10EDA0000F00C1F81500B4F81300A1F81900A07EF7 +:10EDB00091F86B2060F3071281F86B20E07E012848 +:10EDC00018BF002081F83400002078BD91F85E2043 +:10EDD0000420082A08BF81F85E00082D08BF81F8CA +:10EDE0005F00C9E742480068408CC0F3001131B1B0 +:10EDF000C0F38000002804BF1F20704702E0C0F36A +:10EE0000400109B10020704710F0010F14BFEE203F +:10EE1000FF20704736480068408CC0F3001119B1DC +:10EE2000C0F3800028B102E0C0F3400008B1002028 +:10EE30007047012070472E49002209684A664B8CB2 +:10EE40001D2B0CBF81F8682081F8680070470023F3 +:10EE5000274A126882F85D30D164A2F85000012080 +:10EE600082F85D007047224A0023126882F85C3005 +:10EE7000A2F858000120516582F85C0070471C49D7 +:10EE8000096881F8360070471949096881F86100FE +:10EE900070471748006890F961007047144800688F +:10EEA00090F82200C0F3401070471148006890F8B5 +:10EEB0002200C0F3C0007047012070470C48006872 +:10EEC00090F85F00704770B509F018FE09F0F7FD83 +:10EED00009F0C0FC09F06CFD054C2068416E491C2E +:10EEE000416690F83300002558B109F01DFE03E09B +:10EEF000680000200C06002008F007FF206880F85A +:10EF000033502068457090F8391021B1BDE8704049 +:10EF100004200AF0AEBF90F86810D9B1406E81426B +:10EF200018D804200AF0A5FF206890F8220010F0FD +:10EF3000010F07D0A06843220188BDE8704001207E +:10EF4000FFF73CBBBDE8704043224FF6FF71002045 +:10EF5000FFF734BBBDE8704000200AF08ABF2DE9FE +:10EF6000F04782B00F468146FE4E4FF000083068F1 +:10EF7000458C15F0030F10D015F0010F05F00200BD +:10EF800005D0002808BF4FF0010806D004E0002893 +:10EF900018BF4FF0020800D1FFDF4FF0000A5446BF +:10EFA00015F0010F05F002000DD080B915F0040F27 +:10EFB0000DD04AF00800002F1CBF40F0010040F0C7 +:10EFC00002044DD09EE010B115F0040F0DD015F0E5 +:10EFD000070F10D015F0010F05F0020043D00028F4 +:10EFE00008BF15F0040F34D04AE0002F18BF4AF0D4 +:10EFF000090444D141E037B14AF00800044615F055 +:10F00000200F1BD07EE0316805F02002B1F84800E7 +:10F01000104308BF4AF0010474D04AF018000446B7 +:10F0200015F0200F6ED191F85E1011F00C0118BF91 +:10F030000121C94361F30000044663E0316891F89F +:10F040005E1011F00C0118BF012161F300000446AD +:10F0500058E04AF00800002F18BF40F0010451D1D9 +:10F0600040F010044EE0002818BF15F0040F07D040 +:10F07000002F18BF4AF00B0444D14AF0180441E0B5 +:10F0800015F0030F3DD115F0040F3AD077B1306879 +:10F090004AF0080490F85E0010F00C0118BF01213E +:10F0A00061F3410415F0200F24D02BE0306805F007 +:10F0B0002002B0F84810114308BF4AF0030421D0E1 +:10F0C0004AF0180415F0200F0AD000BF90F85E0037 +:10F0D00010F00C0018BF0120C04360F3410411E0A0 +:10F0E00090F85E1011F00C0118BF0121C94361F3C3 +:10F0F0000004EBE710F00C0018BF012060F30004DF +:10F1000000E0FFDF15F0400F1CD0CFB93168B1F837 +:10F110004800002804BF488C10F0010F0BD110F0FC +:10F12000020F08BF10F0200F05D115F0010F08BF26 +:10F1300015F0020F04D091F85E0010F00C0F01D111 +:10F1400044F040047068A0F800A0017821F020018C +:10F1500001704FF007010EF005FE414670680EF099 +:10F16000F8FF214670680FF000F814F0010F0CD082 +:10F170004FF006034FF000027B4970680EF0CFFF9E +:10F180003068417B70680EF02FFE14F0020F18D02B +:10F19000D6E90010B9F1000F4FF006034FF001025D +:10F1A00007D01C310EF0BBFF012170680EF029FE64 +:10F1B00007E015310EF0B3FF3068017D70680EF086 +:10F1C00020FE14F0040F18BFFFDF14F0080F19D051 +:10F1D000CDF800A03068BDF800200223B0F86A1016 +:10F1E00061F30B02ADF8002090F86B0003220109D7 +:10F1F0009DF8010061F307108DF801006946706801 +:10F200000EF08DFF012F62D13068B0F84810E1B3E5 +:10F2100090F82200C0F34000B8BB70680EF095FF74 +:10F22000401CC7B23068C7F1FF05B0F84820B0F8FD +:10F230005A10511AA942B8BF0D46AA423BD990F8BC +:10F24000220010F0010F36D144F0100421467068FE +:10F250000EF08BFFF81CC0B2ED1E284482B230685D +:10F26000B0F86A10436EC1F30B0151FA83F190F8C4 +:10F2700060303E4F1944BC460023E1FB07C31B0925 +:10F280006FF0240C03FB0C1100E020E080F860100C +:10F2900090F85F00012101F01FF90090BDF8000017 +:10F2A0009DF80210032340EA01400190042201A9C5 +:10F2B00070680EF034FF3068AAB2416C70680EF0CE +:10F2C00082FF3068B0F85A102944A0F85A1014F0A0 +:10F2D000400F06D0D6E900100123062261310EF05E +:10F2E0001EFF14F0200F18BFFFDF0020002818BFFA +:10F2F000FFDF02B0BDE8F0872DE9F043194C89B07B +:10F300002068002808BFFFDF20684178002944D129 +:10F310000178FF2941D0002680F83160A0F85A60BA +:10F32000867080F83960304609F062FB104802AD03 +:10F3300000F1240191E80E1085E80E10D0E90D10BF +:10F34000CDE9061002A809F041FB08F0BCFF2068D7 +:10F3500090F9610009F090F8064809F093F8064822 +:10F360000CE00000680000201A06002053E4B36E91 +:10F37000C8610200D0610200CD61020009F012FBF9 +:10F38000606809F038FB206890F8240010F0010F45 +:10F3900007D0252009F07EF80AE009B00C20BDE86E +:10F3A000F08310F0020F18BF262069D009F072F820 +:10F3B000206890F85E10252008F043FF206880F850 +:10F3C0002C6009F00FFB206890F85E10002009F017 +:10F3D00028F90F21052008F0F8FF206890F82E107A +:10F3E000002901BF90F82F10002990F8220010F09A +:10F3F000040F74D006F0B8FE0546206829468068E0 +:10F4000007F0AAFBDFF82884074690FBF8F008FB1A +:10F4100010704142284606F08EFB2168886097FBF9 +:10F42000F8F04A68104448600EF062FA014620681D +:10F43000426891426ED8C0E90165FE4D4FF0010867 +:10F4400095F82D000EF063FA814695F82F000127FC +:10F45000002818BFB9F1000F04D095F82D000EF068 +:10F460008AF8A0B195F8300000281CBF95F82E004E +:10F47000002824D0687B05F10E01012815D019E081 +:10F4800010F0040F14BF2720FFDF8FD190E73A461A +:10F490006F7305F10E0148460AF0B6F895F82D1085 +:10F4A00005F10E000EF035FB09E0487900F0C000D0 +:10F4B000402815D0414605F10E000AF0DDF820681D +:10F4C00090F8220010F0040F24D095F82D000EF0D3 +:10F4D000EDF805001ED0102101F09AFC40B119E0B2 +:10F4E0000EF054FB3A4605F10E010AF08DF8E6E7FE +:10F4F00020683A4600F11C01C77628460AF084F8D5 +:10F50000206800F11C0160680EF060FC0121606859 +:10F510000EF077FC2068417B0E3008F038FF206841 +:10F5200090F85C1061B3B0F85810A0F84810416D25 +:10F53000416490F82210C1F30011F1B9B0F86A00EB +:10F540000221C0F30B05ADF80050684607F0B0FF8C +:10F5500028B1BDF80000C0F30B00A84204D1BDF8EB +:10F560000000401CADF800002168BDF80000B1F8B3 +:10F570006A2060F30B02A1F86A20206880F85C60C2 +:10F58000206890F85D1039B1B0F85010A0F8401024 +:10F59000C16CC16380F85D60B0F86A10426EC1F35F +:10F5A0000B0151FA82F190F86020DFF88CC211440F +:10F5B00063460022E1FB0C3212096FF0240302FBC8 +:10F5C000031180F860100EF00CFA032160680EF051 +:10F5D00090FA216881F8330009B00020BDE8F0837B +:10F5E0009649886070472DE9F043944C83B02268B7 +:10F5F00092F831303BB1508C1D2808BFFFDF03B0BB +:10F60000BDE8F0435FE401260027F1B1054692F81A +:10F61000600008F03FFF206890F85F10FF2008F0BE +:10F6200010FE20684FF4A57190F85F20002009F0CB +:10F63000D4F8206890F8221011F0030F00F02C810C +:10F64000002D00F0238100F027B992F822108046A7 +:10F65000D07EC1F30011002956D0054660680780AE +:10F66000017821F020010170518C132937D01FDC63 +:10F67000102908BF022144D0122908BF062140D01A +:10F68000FFDF6C4D606805F10E010EF091FB697BA8 +:10F6900060680EF0A9FB2068418C1D2918BF152950 +:10F6A00063D0B0F84820416C60680EF0B6FB5CE0B7 +:10F6B000152918BF1D29E3D14FF001010EF052FBAF +:10F6C0006068017841F020010170216885B11C312A +:10F6D0000EF07CFB012160680EF093FBD1E7002166 +:10F6E0000EF040FB6068017841F020010170C8E72E +:10F6F00015310EF06BFB2068017D60680EF081FB18 +:10F70000BFE70EF02FFBBCE70021FFF728FC606885 +:10F71000C17811F03F0F28D0017911F0100F24D0DB +:10F720000EF01EFB2368024693F82410C1F38000FC +:10F73000C1F3400C604401F00101084493F82C101F +:10F74000C1F3800CC1F34005AC4401F001016144F8 +:10F75000401AC1B293F85E0000F0BEFE0090032391 +:10F760000422694660680EF0DAFC2068002590F8F3 +:10F77000241090F82C0021EA000212F0010F18BFAB +:10F7800001250ED111F0020F04D010F0020F08BFB6 +:10F79000022506D011F0040F03D010F0040F08BFAB +:10F7A0000425B8F1000F2BD0012D1BD0022D08BF6E +:10F7B00026201BD0042D14BFFFDF272016D0206881 +:10F7C00090F85E10252008F03CFD206890F822108B +:10F7D000C1F3001169B101224FF49671002008F0C5 +:10F7E000FCFF0DE0252008F055FEE8E708F052FE8A +:10F7F000E5E790F85E204FF49671002008F0EDFFE9 +:10F80000206890F82C10294380F82C1090F82420C0 +:10F8100032EA01011CD04670418C13292BD026DC22 +:10F82000102904BF03B0BDE8F083122923D007E0FC +:10F8300040420F000C06002053E4B36E6800002025 +:10F84000C1F30010002818BFFFDF03B0BDE8F0834C +:10F85000418C1D2908BF80F82C70DCD0C1F3001149 +:10F86000002914BF80F8316080F83170D3E7152982 +:10F8700018BF1D29DBD190F85E2003B04FF00101C5 +:10F88000BDE8F043084609F094B900BF90F85F2046 +:10F890000121084609F08DF92168002DC87E7CD031 +:10F8A0004A8C3D46C2F34000002808BF47F00805D7 +:10F8B00012F0400F18BF45F04005002819BFD1F8DD +:10F8C0003C90B1F84080D1F84490B1F8488060682D +:10F8D000072107800EF046FA002160680EF039FC1F +:10F8E000294660680EF041FC15F0080F17D020681B +:10F8F000BDF800100223B0F86A2062F30B01ADF8E6 +:10F90000001090F86B00032201099DF8010061F3DB +:10F9100007108DF80100694660680EF000FC606811 +:10F920000EF0DCFA2168C0F1FE00B1F85A20A8EB15 +:10F9300002018142A8BF0146CFB2D019404544D24E +:10F9400045F0100160680EF010FC60680EF0C6FA19 +:10F950002168C0F1FE00B1F85A10A8EB0101814204 +:10F96000A8BF0146CFB260680EF0EFFB3844421CDE +:10F970002068B0F86A10436EC1F30B0151FA83F1AD +:10F9800090F86030FE4D1944AC460023E1FB05C3FE +:10F990004FEA131C6FF0240300E03CE00CFB031162 +:10F9A00080F8601090F85F00012100F095FD009054 +:10F9B000BDF800009DF80210032340EA01400190C9 +:10F9C000042201A960680EF0AAFB216891F82200C8 +:10F9D00010F0400F05D001230622613160680EF05F +:10F9E0009EFB20683A46B0F85A0000EB09016068B7 +:10F9F0000EF0E9FB2068B0F85A103944A0F85A100C +:10FA000009F0BFFC002818BFFFDF20684670867031 +:10FA100003B0BDE8F0830121FFF7A1FAF0E7D94870 +:10FA200010B50068417841B90078FF2805D0002161 +:10FA30000846FFF7D8FD002010BD09F05FF809F077 +:10FA40003EF808F007FF08F0B3FF0C2010BD2DE9C9 +:10FA5000F041CC4D0446174628680E4690F86C00DD +:10FA6000002818BFFFDF2868002F80F86E7018BFCD +:10FA7000BDE8F0812188A0F870106188A0F8861098 +:10FA8000A188A0F88810E188A0F88A1094F888115D +:10FA900080F88C1090F82F10002749B1427B00F1BC +:10FAA0000E01012A04D1497901F0C001402935D065 +:10FAB00090F8301041B1427B00F10E01012A04BFE1 +:10FAC000497911F0C00F29D000F17A00F3F794FAC8 +:10FAD0006868FF2E0178C1F380116176D0F80310B9 +:10FAE000C4F81A10B0F80700E08328681ED0C0F8E8 +:10FAF0008010E18BA0F8841000F17402511E304692 +:10FB00000DF014FF002808BFFFDF286890F873107D +:10FB100041F0020180F87310BDE8F081D0F80E10BA +:10FB2000C0F87A10418AA0F87E10D1E7C0F8807042 +:10FB3000A0F88470617E80F87310D4F81A104167C1 +:10FB4000E18BA0F87810BDE8F08170B58D4C0125EF +:10FB5000206890F82200C0F3C00038B13C22FF2199 +:10FB6000A068FFF774FF206880F86C50206890F858 +:10FB7000220010F0010F1CBFA06801884FF03C026A +:10FB800012BF01204FF6FF710020FEF717FD20681D +:10FB900080F8395070BD7B49096881F832007047A0 +:10FBA0002DE9F041774C0026206841780127354641 +:10FBB000012906D0022901D003297DD0FFDFBDE84D +:10FBC000F081817802250029418C46D0C1F34002A2 +:10FBD000002A08BF11F0010F6FD090F85F204FF09E +:10FBE00001014FF0000008F0E4FF216891F82200C5 +:10FBF000C0F34000002814BF0C20222091F85F10B1 +:10FC000008F01FFB2068457090F8330058B108F0E9 +:10FC100068F8206890F85F0010F00C0F0CBF4020CF +:10FC2000452008F077FF206890F83400002818BFBE +:10FC300008F08FFF216891F85F0091F8691010F0CB +:10FC40000C0F08BF0021962008F0F6FE09F090FB8B +:10FC5000002818BFFFDFBDE8F081C1F3001282B1B8 +:10FC600010293FD090F8330020B108F03AF8402036 +:10FC700008F050FF206890F8221011F0040F36D0E1 +:10FC800043E090F8242090F82C309A422AD1B0F822 +:10FC90004800002808BF11F0010F05D111F0020F34 +:10FCA00008BF11F0200F6FD04FF001014FF000009E +:10FCB000FFF799FC206801E041E035E0418C11F04C +:10FCC000010F04BFC1F34001002907D1B0F85A1059 +:10FCD000B0F84820914218BFBDE8F08180F831703B +:10FCE000BDE8F081BDE8F041002101207BE490F8FF +:10FCF0003710012914BF0329102646F00E0190F891 +:10FD00005E204FF0000008F054FF206890F83400A7 +:10FD1000002818BF08F01DFF0021962008F08CFE77 +:10FD200020684570BDE8F081B0F85A10B0F848007E +:10FD3000814242D0BDE8F0410121084653E4817878 +:10FD4000D9B1418C11F0010F22D080F86C7090F87D +:10FD50006E20B0F870100120FEF730FC206845706E +:10FD600008F0CCFE08F0ABFE08F074FD08F020FEB1 +:10FD7000BDE8F04103200AF07CB88178012004E05E +:10FD800053E4B36E6800002017E0BDE8F0412AE4B8 +:10FD900011F0020F04BFFFDFBDE8F081B0F85A1088 +:10FDA000B0F84000814208D001210846FFF71BFC53 +:10FDB000216803204870BDE8F081BDE8F041FFF7FD +:10FDC00082B8FFF780B810B5FE4C206890F8341068 +:10FDD00049B1383008F0CCFE18B921687F2081F88D +:10FDE000380008F0ACFE206890F8330018B108F035 +:10FDF0009BFE07F08AFF0AF02EFCA8B1206890F85D +:10FE00002210C1F3001179B14078022818BFFFDF3A +:10FE100000210120FFF7E7FB2068417800291EBF81 +:10FE200040780128FFDF10BDBDE81040FFF74BB858 +:10FE30002DE9F047E34C0F4680462168B8F1030FE7 +:10FE4000488C08BFC0F3400508D000F0010591F8C8 +:10FE50003200002818BF4FF0010901D14FF000090E +:10FE600008F00CFB0646B8F1030F0CBF4FF0020878 +:10FE70004FF0010835EA090008BFBDE8F0872068A7 +:10FE800090F8330068B10DF08FFD38700146FF28FF +:10FE900007D06068C01C0DF060FD38780DF091FD52 +:10FEA000064360680178C1F3801221680B7D9A4295 +:10FEB00008D10622C01C1531FEF792FA002808BFAF +:10FEC000012000D000203978FF2906D0C8B9206869 +:10FED00090F82D00884216D113E0A0B1616811F8A6 +:10FEE000030BC0F380100DF006FD05460DF061FE1A +:10FEF00038B128460DF0DAFB18B1102100F088FF68 +:10FF000008B1012000E00020216891F8221011F0D2 +:10FF1000040F01D0F0B11AE0CEB9AB4890F8370029 +:10FF2000002818BF404515D1616811F8030BC0F3D4 +:10FF300080100DF0E0FC04460DF03BFE38B1204689 +:10FF40000DF0B4FB18B1102100F062FF10B10120D8 +:10FF5000BDE8F0870020BDE8F0872DE9F04F994D0E +:10FF6000044683B0286800264078022818BFFFDFC7 +:10FF700028684FF07F0B90F8341049B1383008F002 +:10FF8000F7FD002804BF286880F838B008F0D7FDD6 +:10FF900068680DF009FF8046002C00F0458208F0EB +:10FFA00010FA002800F04082012400274FF0FF09DA +:10FFB000B8F1050F1ED1686890F8240000F01F000A +:10FFC000102817D9286890F8360098B18DF800905D +:10FFD00069460520FFF72CFF002800F025822868DD +:10FFE00080F8A64069682222A730C91CFEF725FACE +:10FFF00000F01ABA68680EF062F8002800F0148267 +:020000040001F9 +:100000004046DFF8C4814FF0030A062880F02182C1 +:10001000DFE800F0FCFCFC03FCFB8DF80090694677 +:100020000320FFF705FF002800F0F180296891F810 +:10003000340010B191F89C00D8B12868817801296A +:100040004DD06868042107800DF08CFE08F10E0188 +:1000500068680DF0ADFE98F80D1068680DF0C4FEEC +:100060002868B0F84020C16B68680DF0FAFE00F017 +:1000700063B99DF8000081F89C400A7881F89D20C2 +:10008000FF280FD001F19F029E310DF04FFC002898 +:1000900008BFFFDF286890F89E1041F0020180F849 +:1000A0009E100DE068680278C2F3801281F89E20ED +:1000B000D0F80320C1F89F20B0F80700A1F8A300F2 +:1000C000286800F1A50490F838007F2808BFFFDFFA +:1000D000286890F83810217080F838B0ADE790F8B3 +:1000E00022000721C0F3801938480479686869F351 +:1000F000861407800DF036FE002168680EF029F89E +:10010000214668680EF031F80623002208F10E013E +:1001100068680EF004F82868417B68680DF064FE9A +:1001200068680DF0DBFE2968B1F84020C0F1FE01DF +:100130008A42B8BF1146CFB2BA423CD9F81EC7B204 +:1001400044F0100B594668680EF00FF868680DF01F +:10015000FCFF384400F101082868B0F86A10426ECC +:10016000C1F30B0151FA82F190F86020184C0A4457 +:10017000A4460023E2FB04C319096FF0240301FB2A +:10018000032180F8601090F85F004246012100F0E2 +:10019000A3F90190BDF804009DF80610032340EA7E +:1001A00001400290042202A968680DF0B8FF594688 +:1001B00068680DF0DAFFB9F1000F0FD0D5E9001033 +:1001C000012307E0680000200C060020C86102003F +:1001D00053E4B36E062261310DF0A1FF28683A4660 +:1001E000C16B68680DF0EFFF2868A0F85A70B0F88E +:1001F00040108F420CBF0121002180F8311009F01E +:10020000C0F8002818BFFFDF96E007E021E128686A +:100210008078002840F00A8100F006B98DF800903F +:1002200068680178C1F38019D0F803100191B0F823 +:100230000700ADF8080069460520FFF7F9FD002822 +:1002400028687DD0817800297CD090F85FB0D5E90E +:100250000104D0F80F10C4F80E10B0F8131061822A +:10026000417D2175817D6175B0F81710E182B0F88C +:1002700019106180B0F81B10A180B0F81D10E1804A +:1002800000F11F0104F1080015F085FE686890F880 +:10029000241001F01F01217690F82400400984F811 +:1002A000880184F864B084F865B01BF00C0F0CBFB3 +:1002B0000021012104F130000EF0ABF92868002282 +:1002C00090F8691084F8661090F8610084F867006F +:1002D0009DF80010A868FFF7BAFB022009F0C9FDDD +:1002E000B2480DF1040B08210468686807800DF01E +:1002F00039FD002168680DF02CFF214668680DF07B +:1003000034FF0623002208F10E0168680DF007FF94 +:100310002868417B68680DF067FD494668680DF004 +:1003200070FD06230122594668680DF0F8FE09F0B9 +:1003300028F8002818BFFFDF286880F801A077E0C0 +:100340006DE0FFE76868D5F808804FF00109027892 +:1003500098F80D10C2F34012114088F80D10D0F833 +:100360000F10C8F80E10B0F81310A8F81210417D45 +:1003700088F81410817D88F81510B0F81710A8F8C7 +:100380001610B0F81910A8F80210B0F81B10A8F851 +:100390000410B0F81D10A8F8061000F11F0108F1B4 +:1003A000080015F0F8FD686890F8241001F01F01AE +:1003B00088F8181090F824000021400988F8880176 +:1003C00088F8649088F8659008F130000EF021F903 +:1003D0002868002290F8691088F8661090F861008B +:1003E00088F867009DF80010A868FFF730FB2868C0 +:1003F00080F86C4090F86E20B0F870100120FEF785 +:10040000DDF82868477008F079FB08F058FB08F021 +:1004100021FA08F0CDFA012009F02BFD08E090F850 +:100420002200C0F3001008B1012601E0FEF74BFDE9 +:10043000286890F8330018B108F076FB07F065FCE7 +:1004400096B10AF008F960B100210120FFF7CBF85E +:1004500013E0286890F82200C0F300100028E5D0CF +:10046000E2E7FEF730FD08E028688178012904D131 +:1004700090F85F10FF2007F0E4FE2868417800291B +:1004800019BF4178012903B0BDE8F08F40780328F7 +:1004900018BFFFDF03B0BDE8F08F70B5444C0646CF +:1004A0000D462068807858B107F0F2FD21680346B8 +:1004B000304691F85F202946BDE870400AF085BAC1 +:1004C00007F0E6FD21680346304691F85E20294694 +:1004D000BDE870400AF079BA78B50C460021009169 +:1004E000082804BF4FF4C87040210DD0042804BF71 +:1004F0004FF4BF70102107D0022807BF01F1180088 +:10050000042101F128000821521D02FB01062848A0 +:100510009DF80010006890F8602062F3050141F03A +:1005200040058DF8005090F85F00012829D002287E +:100530002ED004281CBF0828FFDF2FD025F0800014 +:100540008DF80000C4EB041000EB80004FF01E019A +:1005500001EB800006FB04041648844228BFFFDF3D +:100560001548A0FB0410BDF80110000960F30C0150 +:10057000ADF80110BDF800009DF8021040EA0140FE +:1005800078BD9DF8020020F0E0008DF80200D5E76C +:100590009DF8020020F0E000203004E09DF8020009 +:1005A00020F0E00040308DF80200C7E7C86102008B +:1005B00068000020C4BF0300898888880023C383A3 +:1005C000428401EBC202521EB2FBF1F1018470477A +:1005D0002DE9F04104460026D9B3552333224FF4C8 +:1005E000FA4501297DD0022900F01481032918BFA2 +:1005F000BDE8F08104F17001207B00F01F00207342 +:1006000084F889605FF0000004EB000C9CF808C0DF +:1006100003EA5C05ACEB050C0CF0FF0C0CF03305A9 +:1006200002EA9C0CAC440D180CEB1C1C0CF00F0CDB +:1006300085F814C04D7E401CAC44C0B281F819C08E +:100640000528E1D30CF0FF00252898BFBDE8F08114 +:10065000DCE0FFE704F17005802200212846FDF769 +:1006600016FFAE71EE712E736E73EE732E746E7193 +:10067000AE76EE76212085F84000492085F84100CD +:10068000FE2085F874002588702200212046FDF7A1 +:10069000FEFE2580012584F8645084F865502820EA +:1006A00084F86600002104F130000DF0B2FF1B2237 +:1006B000A4F84E20A4F85020A4F85220A4F8542006 +:1006C0004FF4A470A4F85600A4F8580065734FF4D2 +:1006D00048606080A4F8F060A4F8F260A4F8F460C8 +:1006E00000E023E0A4F8F660A4F8F86084F8FA606B +:1006F00084F8FD60A4F8066184F80461A4F8186128 +:10070000A4F81A6184F8B66184F8B76184F8C0610E +:1007100084F8C16184F88C6184F88F6184F8A861E1 +:10072000C4F8A061C4F8A461BDE8F081A4F8066132 +:1007300084F8FB606088FE490144B1FBF0F1A4F845 +:1007400090104BF68031A4F89210B4F806C0A4F8CB +:100750009860B4F89C704FEACC0C4743BCFBF0FCAB +:1007600097FBF0F70CF1010CA4F89C701FFA8CFCBD +:100770000CFB00F704F17001A4F89AC0B7F5C84F5C +:10078000C4BFACF1010CA1F82AC0B5FBF0FC0CF120 +:10079000010CA1F830C000F5802C0CF5EE3CACF15A +:1007A0000105B5FBF0FCA1F820C0CD8B05FB00FCDA +:1007B000BCFBF0F0C8830846217B01F01F012173C8 +:1007C0004676002104EB010C9CF808C003EA5C05A6 +:1007D000ACEB050C0CF0FF0C0CF0330502EA9C0CA2 +:1007E000AC4445180CEB1C1C0CF00F0C85F814C025 +:1007F000457E491CAC44C9B280F819C00529E1D333 +:100800000CF0FF00252898BFBDE8F081FFDFBDE8B0 +:10081000F08100BFB4F8B011B4F8B4316288A4F824 +:100820009860B4F89CC0DB000CFB02FCB3FBF1F356 +:100830009CFBF1FC5B1CA4F89CC09BB203FB01FC7D +:1008400004F17000A4F89A30BCF5C84FC4BF5B1E19 +:100850004385B5FBF1F35B1C0386438C01EBC303BB +:100860005B1EB3FBF1F30384C38B5A43B2FBF1F17C +:10087000C183BDE8F0812DE9F04104460025A1B314 +:1008800055234FF4FA464FF0330C01297DD002294D +:1008900000F0E080032918BFBDE8F08104F170008A +:1008A000217B01F01F01217384F889500021621817 +:1008B000127A03EA5205521BD2B202F033050CEA57 +:1008C00092022A44451802EB121202F00F022A7516 +:1008D000457E491C2A44C9B242760529E7D3D0B2E5 +:1008E000252898BFBDE8F081B1E0FFE704F170066C +:1008F000802200213046FDF7CAFDB571F5713573D0 +:100900007573F57335747571B576F576212086F8B3 +:100910004000492086F84100FE2086F874002688B1 +:10092000702200212046FDF7B2FD2680012684F8C2 +:10093000646084F86560282084F86600002104F172 +:1009400030000DF066FE1B22A4F84E20A4F85020C3 +:10095000A4F85220A4F854204FF4A470A4F8560030 +:10096000A4F858006673A4F8F850202084F8FA0020 +:1009700084F8F050C4F8F45084F8245184F82551D8 +:1009800084F82E5184F82F5100E005E084F81451CA +:1009900084F82051BDE8F081618865480844B0FBC7 +:1009A000F1F0A4F890004BF68030A4F89200E288B1 +:1009B000A4F89850B4F89C70D2004F43B2FBF1F207 +:1009C00097FBF1F7521CA4F89C7092B202FB01F75E +:1009D00004F17000A4F89A20B7F5C84FC4BF521EA6 +:1009E0004285B6FBF1F2521C028601F5802202F527 +:1009F000EE32561EB6FBF1F20284C68B06FB01F204 +:100A0000B2FBF1F1C1830146207B00F01F0020738F +:100A10004D7600202218127A03EA5205521BD2B2F8 +:100A200002F033050CEA92022A440D1802EB12126E +:100A300002F00F022A754D7E401C2A44C0B24A764D +:100A40000528E7D3D0B2252898BFBDE8F081FFDFA5 +:100A5000BDE8F081D0F81811628804F1700348896C +:100A6000C989A4F89850B4F89CC0C9000CFB02FCDA +:100A7000B1FBF0F19CFBF0FC491CA4F89CC089B2CE +:100A800001FB00FCA4F89A10BCF5C84FC4BF491E76 +:100A90005985B6FBF0F1491C1986598C00EBC10150 +:100AA000491EB1FBF0F11984D98B5143B1FBF0F031 +:100AB000D883BDE8F0812DE9F003447E0CB1252CEC +:100AC00003D9BDE8F00312207047002A02BF0020BE +:100AD000BDE8F003704791F80DC01F260123154DA6 +:100AE0004FF00008BCF1000F7AD0BCF1010F1EBF1F +:100AF0001F20BDE8F0037047B0F800C00A7C8F7B70 +:100B000091F80F907A404F7C87EA090742EA072262 +:100B100082EA0C0C5FF000070CF0FF0999FAA9F9C2 +:100B20004FEA1C2C4FEA19699CFAACFC04E0000067 +:100B3000FFDB050053E4B36E4FEA1C6C49EA0C2C52 +:100B40000CEB0C1C7F1C9444FFB21FFA8CFC032F8F +:100B5000E2D38CEA020CFB4F0022ECFB0572120977 +:100B60006FF0240502FB05C2D2B201EBD2078276F8 +:100B700002F007053F7A03FA05F52F4218BFC27647 +:100B80007ED104FB0CF2120C521CD2B25FF00004B6 +:100B900000EB040C9CF814C094453CBFA2EB0C0283 +:100BA000D2B212D30D194FF0000C2D7A03FA0CF7C4 +:100BB0003D421CBF521ED2B2002A69D00CF1010C7A +:100BC0000CF0FF0CBCF1080FF0D304F1010C0CF099 +:100BD000FF04052CDCD33046BDE8F0037047FFE787 +:100BE00090F81AC00C7E474604FB02C2D54C4FF069 +:100BF000000CE2FB054C4FEA1C1C6FF024040CFBBC +:100C00000422D2B201EBD204827602F0070C247ADD +:100C100003FA0CFC14EA0C0F1FBFC2764046BDE875 +:100C2000F003704790F819C0B2FBFCF40CFB1422DF +:100C3000521CD2B25FF0000400EB040C9CF814C00C +:100C400094453CBFA2EB0C02D2B212D30D194FF067 +:100C5000000C2D7A03FA0CF815EA080F1CBF521E7F +:100C6000D2B272B10CF1010C0CF0FF0CBCF1080F08 +:100C7000F0D304F1010C0CF0FF04052CDCD3AAE73F +:100C800009E00CEBC401C1763846BDE8F0037047BB +:100C90000CEBC401C1764046BDE8F0037047AA4A98 +:100CA000016812681140A94A126811430160704737 +:100CB00030B4A749A44B00244FF0010C0A78521C11 +:100CC000D2B20A70202A08BF0C700D781A680CFA8C +:100CD00005F52A42F2D0097802680CFA01F1514078 +:100CE000016030BC704770B46FF01F02010C02EA63 +:100CF00090251F23A1F5AA4054381CBFA1F5AA4096 +:100D0000B0F1550009D0A1F52850AA381EBFA1F5B1 +:100D10002A40B0F1AA00012000D100204FF0000CC1 +:100D2000624601248CEA0106F6431643B6F1FF3F02 +:100D300011D005F001064FEA5C0C4CEAC63C03F00A +:100D4000010652086D085B08641C42EAC632162C84 +:100D5000E8DD70BC704770BC0020704790F804C09C +:100D60003CF01F011CBF0020704730B401785522B1 +:100D700002EA5103C91AC9B201F03304332303EA6A +:100D800091012144447801EB111102EA5405641BDE +:100D9000E4B204F0330503EA94042C4404EB141485 +:100DA00001F00F0104F00F040C448178C07802EACE +:100DB0005105491BC9B201F0330503EA91012944E9 +:100DC00001EB111101F00F01214402EA5004001B54 +:100DD000C0B200F0330403EA9000204400EB10108E +:100DE00000F00F00014402EA5C00ACEB0000C0B26E +:100DF00000F0330203EA9000104400EB101000F002 +:100E00000F00084401288CBF0120002030BC70472F +:100E10000A000ED00123012A0BDB491EC9B210F8CB +:100E200001C0BCF1000F01D0002070475B1C934251 +:100E3000F3DD01207047002A08BF70471144401EAF +:100E400012F0010F03D011F8013D00F8013F5208E4 +:100E500008BF704711F8013C437011F8023D00F8DB +:100E6000023F521EF6D1704770B58CB000F11004ED +:100E70001D4616460DF1FF3C5FF0080014F8012CEA +:100E80008CF8012014F8022D0CF8022F401EF5D129 +:100E900001F1100C6C460DF10F0108201CF8012C1B +:100EA0004A701CF8022D01F8022F401EF6D1204690 +:100EB00013F01CFB7EB16A1E04F130005FF00801E4 +:100EC00010F8013C537010F8023D02F8023F491E31 +:100ED000F6D10CB070BD08982860099868600A982F +:100EE000A8600B98E8600CB070BD38B505460C469C +:100EF000684607F03DFE002808BF38BD9DF9002078 +:100F00002272E07E607294F90A100020511A48BFE4 +:100F1000494295F82D308B42C8BF38BDFF2B08BF22 +:100F200038BDE17A491CC9B2E17295F82E30994278 +:100F300003D8A17A7F2918BF38BDA2720020E072C1 +:100F4000012038BD53E4B36E04620200086202005F +:100F5000740000200C2818BF0B2810D00D2818BFD3 +:100F60001F280CD0202818BF212808D0222818BFFD +:100F7000232804D024281EBF2628002070474FF0C5 +:100F8000010070470C2963D2DFE801F006090E1357 +:100F9000161B323C415C484E002A5BD058E0072AC1 +:100FA00018BF082A56D053E00C2A18BF0B2A51D07C +:100FB0004EE00D2A4ED04BE0A2F10F000C2849D98B +:100FC00046E023B1A2F110000B2843D940E0122AD9 +:100FD00018BF112A3ED090F8380020B1122A37D31A +:100FE0001A2A37D934E0162A32D31A2A32D92FE0F6 +:100FF000A2F10F0103292DD990F8380008B31B2A5C +:1010000028D925E0002B08BF042A21D122E013B102 +:10101000062A1FD01CE0012A1AD11BE01C2A1CBF83 +:101020001D2A1E2A16D013E01F2A18BF202A11D00D +:10103000212A18BF222A0DD0232A1CBF242A262A9F +:1010400008D005E013B10E2A04D001E0052A01D032 +:1010500000207047012070472DE9F0410D460446FD +:10106000866805F02FFA58B905F07EF840F236711F +:1010700004F061FDA060204605F024FA0028F3D0BA +:1010800095B13046A16805F067FD00280CDD2844C5 +:10109000401EB0FBF5F707FB05F1304604F04BFDB1 +:1010A000A0603846BDE8F0810020BDE8F08170B551 +:1010B0000446904228BF70BD101B64280BD325182E +:1010C0008D4206D8042105F07AFD00281CBF284671 +:1010D00070BD204670BD6420F1E711F00C0F13D0F5 +:1010E00001F0040100290DBF4022102296214FF487 +:1010F000167101F5BC71A0EB010388428CBF93FB14 +:10110000F2F0002080B27047022919BF6FF00D0184 +:1011100001EBD0006FF00E0101EB9000F2E7084404 +:1011200018449830002A14BF042100210844704755 +:1011300010B4002A14BF4FF429624FF4A472002B9C +:1011400019BF4FF429634FF0080C4FF4A4734FF00C +:10115000010C00280CBF0124002491F866001CF04B +:101160000C0F08BF0020D11808449830002C14BF81 +:1011700004210021084410BC704700280CBF012343 +:10118000002391F86600002BA0F6482000F50050DF +:1011900018BF04231844496A81422CBF0120002053 +:1011A00012F00C0118BF012131EA000014BF002029 +:1011B0000120704710B413680B66137813F00C030A +:1011C00018BF0123527812F00C0218BF012253EA13 +:1011D000020C04BF10BC7047002B0CBF4FF4A4736B +:1011E0004FF42963002A19BF4FF429624FF0080C0D +:1011F0004FF4A4724FF0010C00280CBF012400240E +:1012000091F866001CF00C0F08BF00201A4410442F +:101210009830002C14BF0422002210444A6A8242F3 +:1012200024BF10BC704791F860004FF0030230F00B +:101230000C0381F8603091F8610020F00C0081F817 +:10124000610008BF81F86020002808BF81F8612094 +:1012500010BC704710F0010F1CBF0120704710F048 +:10126000020F1CBF0220704710F0040018BF0820B6 +:1012700070472DE9F0470446174689464FF00108AC +:1012800008460DF0FAF8054648460DF0FAF810F059 +:10129000010F18BF012624D015F0010F18BF01233C +:1012A0002AD000BF56EA030108BF4FF0000810F033 +:1012B000070F08BF002615F0070F08BF002394F89A +:1012C0006400B0420CBF00203046387094F86510BE +:1012D000994208BF00237B70002808BF002B25D14E +:1012E00015E010F0020F18BF0226D5D110F0040F40 +:1012F00014BF08260026CFE715F0020F18BF0223FF +:10130000D0D115F0040F14BF08230023CAE74846C4 +:101310000DF0BDF8B4F87010401A00B247F6FE7137 +:10132000884201DC002801DC4FF0000816B1082ECD +:101330000CD018E094F86400012818BF022812D0DD +:1013400004281EBF0828FFDF032D0CD194F8C0012C +:1013500048B1B4F8C401012894F8640006D0082804 +:1013600001D0082038704046BDE8F087042818BF37 +:101370000420F7D1F5E7012814BF0228704710F0C8 +:101380000C0018BF0420704738B4CBB2C1F3072C4F +:10139000C1B2C0F30724012B07D0022B09D0042BC4 +:1013A00008BFBCF1040F2DD006E0BCF1010F03D142 +:1013B00028E0BCF1020F25D0012906D0022907D070 +:1013C000042908BF042C1DD004E0012C02D119E02F +:1013D000022C17D001EA0C0161F3070204EA0301B1 +:1013E00061F30F22D1B211F0020F18BF022310D007 +:1013F000C2F307218DF8003011F0020F18BF02214F +:101400001BD111E0214003EA0C03194061F30702EC +:10141000E6E711F0010F18BF0123E9D111F0040F25 +:1014200014BF08230023E3E711F0010F18BF0121C7 +:1014300003D111F0040118BF08218DF80110082B09 +:1014400001BF000C012804208DF80000BDF8000049 +:1014500038BC70474FF0000C082902D0042909D08D +:1014600011E001280FD10420907082F803C013808E +:1014700001207047012806D00820907082F803C030 +:1014800013800120704700207047162A10D12A22AD +:101490000C2818BF0D280FD04FF0230C1F280DD09B +:1014A00031B10878012818BF002805D0162805D0CA +:1014B00000207047012070471A70FBE783F800C0D6 +:1014C000F8E7012908D002290BD0042912BF082906 +:1014D00040F6A660704707E0002804BF40F2E240F3 +:1014E000704740F6C410704700B5FFDF40F2E2409D +:1014F00000BD00000178406829B190F82C1190F8E7 +:101500008C0038B901E001F0BDBD19B1042901D04A +:10151000012070470020704770B50C460546062133 +:1015200002F0C4FC606008B1002006E007212846F4 +:1015300002F0BCFC606018B101202070002070BD7A +:10154000022070BD2DE9FC470C4606466946FFF7B0 +:10155000E3FF00287DD19DF8000050B1FDF7EEF8C3 +:10156000B0427CD0214630460AF008FC002873D1F6 +:101570002DE00DF097F9B04271D02146304612F0BF +:10158000B6FA002868D1019D95F8F00022E001200C +:1015900000E00020804695F839004FF0010A4FF036 +:1015A0000009F0B195F83A0080071AD584F8019047 +:1015B00084F800A084F80490E68095F83B1021722E +:1015C000A98F6181E98FA18185F8399044E0019D5F +:1015D00095F82C0170350028DBD1287F0028D8D061 +:1015E000D5E7304602F0A5FD070000D1FFDF384601 +:1015F00001F0B5FF40B184F801900F212170E68021 +:10160000208184F804A027E0304602F080FD070026 +:1016100000D1FFDFB8F1000F21D0384601F0F7FF0D +:10162000B8B19DF8000038B90198D0F81801418888 +:10163000B14201D180F80090304607F00DFF84F8E8 +:1016400001900C21217084F80490E680697F21725A +:1016500000E004E085F81C900120BDE8FC87002034 +:10166000FBE71CB56946FFF757FF00B1FFDF68468F +:1016700001F014FDFE4900208968A1F8F2001CBDAC +:101680002DE9FC4104460E46062002F0B7FB054654 +:10169000072002F0B3FB2844C7B20025A8463E4409 +:1016A00017E02088401C80B22080B04202D3404620 +:1016B000A4F8008080B2B84204D3B04202D2002025 +:1016C000BDE8FC816946FFF727FF0028F8D06D1CB4 +:1016D000EDB2AE42E5D84FF6FF7020801220EFE762 +:1016E00038B54FF6FF70ADF800000DE00621BDF8EB +:1016F000000002F0EDFB04460721BDF8000002F0F7 +:10170000E7FB0CB100B1FFDF00216846FFF7B8FF2F +:101710000028EBD038BD70B507F00CFF0BF034FF9C +:10172000D44C4FF6FF76002526836683D2A0257021 +:1017300001680079A4F14002657042F8421FA11CC3 +:101740001071601C12F0EFFA1B2020814FF4A4717D +:101750006181A081E18107212177617703212174D3 +:10176000042262746082A082A4F13E00E1820570CE +:101770004680BF480C300570A4F11000057046800B +:1017800084F8205070BD70B5B94C16460D466060A7 +:10179000217007F047FEFFF7A3FFFFF7BCFF20789B +:1017A0000FF0BDFFB6480DF0D0F92178606812F057 +:1017B0005FFA20780BF0DCF8284608F0AFFEB0485E +:1017C000FCF7C7FF217860680AF0B2FB3146207849 +:1017D00012F024FDBDE870400BF0D6BE10B5012418 +:1017E0000AB1002010BD21B1012903D000242046F8 +:1017F00010BD02210CF024FDF9E710B50378044672 +:10180000002B406813460A46014609D05FF00100EC +:10181000FFF78EFC6168496A884203D9012010BD38 +:101820000020F5E7002010BD2DE9F04117468A7829 +:101830001E46804642B11546C87838B1044669074D +:1018400006D52AB1012104E00725F5E70724F6E7CC +:101850000021620702D508B1012000E0002001420A +:1018600006D0012211464046FFF7C7FF98B93DE078 +:1018700051B1002201214046FFF7BFFF58B9600770 +:1018800034D50122114620E060B1012200214046FA +:10189000FFF7B3FF10B10920BDE8F081680725D537 +:1018A000012206E068074FEA44700AD5002814DBDD +:1018B000002201214046FFF7A0FFB8B125F0040542 +:1018C00014E0002812DA012200214046FFF795FFBC +:1018D00060B100BF24F0040408E001221146404634 +:1018E000FFF78BFF10B125F00405F3E73D7034706E +:1018F0000020D1E770B58AB0044600886946FFF73A +:101900000BFE002806D1A08830B1012804D002289F +:1019100002D012200AB070BD04AB03AA214668466B +:10192000FFF782FF0500F5D19DF800100120002689 +:101930000029019906D081F8C101019991F80C1292 +:10194000B1BB2DE081F82F01019991F8561139B9F9 +:10195000019991F82E1119B9019991F8971009B1CF +:101960003A2519E00199059681F82E01019A9DF812 +:101970000C0082F83001019B9DF8102083F8312182 +:10198000A388019CA4F832318DF814008DF815203D +:1019900005AA0020FFF70EFC019880F82F6126E0D1 +:1019A000019991F8C01119B9019991F8971009B1ED +:1019B0003A2519E00199059681F8C00101989DF832 +:1019C0000C2080F8C221019B9DF8100083F8C30110 +:1019D000A388019CA4F8C4318DF814208DF815005B +:1019E00005AA0120FFF7E6FB019880F8C1612846AF +:1019F00090E710B504460020A17801B90120E278F3 +:101A00000AB940F0020001F058FB002803D120463B +:101A1000BDE810406EE710BD70B5044691F8650052 +:101A200091F866300D4610F00C0F00D1002321898B +:101A3000A088FFF774FB696A814229D2401A401CD2 +:101A4000A1884008091A8AB2A2802189081A208137 +:101A5000668895F864101046FFF73FFB864200D277 +:101A600030466080E68895F8651020890AE000001D +:101A70007800002018080020FFFFFFFF1F00000073 +:101A8000D8060020FFF729FB864200D23046E080CE +:101A900070BDF0B585B00D46064603A9FFF73CFDC5 +:101AA00000282DD19DF80C0060B300220499FB2082 +:101AB000B1F84E30FB2B00D30346B1F85040FB2069 +:101AC000FB2C00D30446DFF85CC59CE88110009035 +:101AD0000197CDF808C0ADF80230ADF80640684671 +:101AE000FFF79AFF6E80BDF80400E880BDF808009B +:101AF0006881BDF80200A880BDF80600288100209A +:101B000005B0F0BD0122D1E72DE9F04186B00446D1 +:101B100000886946FFF700FD002876D12189E0881A +:101B200001F0E4FA002870D1A188608801F0DEFAA3 +:101B300000286AD12189E08801F0CFFA002864D119 +:101B4000A188608801F0C9FA07005ED1208802A947 +:101B5000FFF79FFF00B1FFDFBDF81010628809207A +:101B6000914252D3BDF80C10E28891424DD3BDF89A +:101B70001210BDF80E2023891144A2881A44914204 +:101B800043D39DF80010019D4FF00008012640F658 +:101B9000480041B185F8B761019991F8F81105F550 +:101BA000DB7541B91AE085F82561019991F84A1170 +:101BB00005F5927509B13A2724E0E18869806188CA +:101BC000E9802189814200D30146A980A188814210 +:101BD00000D208462881012201990FE0E18869803E +:101BE0006188E9802189814200D30146A980A188CA +:101BF000814200D208462881019900222846FFF739 +:101C00000BFF2E7085F80180384606B044E67BE76E +:101C100070B504460CF0FCFDB0B12078182811D145 +:101C2000207901280ED1E088062102F03FF9040056 +:101C300008D0208807F010FC2088062102F048F91F +:101C400000B1FFDF012070BDF74D28780028FAD0E1 +:101C5000002666701420207020223146201DFCF7DB +:101C600016FC022020712E70ECE710B50446FCF73C +:101C7000DBFC002813D0207817280FD1207968B119 +:101C8000E088072102F012F940B1008807F0E4FB78 +:101C9000E088072102F01CF900B1FFDF012010BD30 +:101CA0002DE9F0475FEA000800D1FFDFDE4802219E +:101CB0001A308146FFF7E4FC00B1FFDFDA4C062062 +:101CC000678B02F09BF80546072002F097F828443E +:101CD000C5B2681CC6B2608BB04203D14046FFF764 +:101CE000C4FF58B9608BA84203D14046FFF790FF6C +:101CF00020B9608B4146FFF725FC38B1404601F022 +:101D000003FA0028E7D10120BDE8F0870221484608 +:101D1000FFF7B6FC10B9608BB842DCD14046BDE895 +:101D2000F04712F0C1BA10B501F053F908B10C2018 +:101D300010BD0BF07DFC002010BD10B504460078EE +:101D400018B1012801D0122010BD01F053F920B1C3 +:101D50000BF0C0FD08B10C2010BD207801F013F984 +:101D6000E21D04F11703611CBDE810400BF0DABC62 +:101D700010B5044601F02DF908B10C2010BD2078F3 +:101D800028B1012803D0FF280BD0122010BD01F08C +:101D9000FAF8611C0BF00CFC08B1002010BD072004 +:101DA00010BD01200BF03EFCF7E710B50BF095FDE0 +:101DB00008B1002010BD302010BD10B5044601F060 +:101DC00019F908B10C2010BD20460BF080FD002051 +:101DD00010BD10B501F00EF920B10BF07BFD08B17C +:101DE0000C2010BD0BF0F6FC002010BDFF2181700F +:101DF0004FF6FF7181808D4949680A7882718A881F +:101E000002814988418101214170002070477CB5E1 +:101E10000025022A19D015DC12F10C0F15D009DCAF +:101E200012F1280F11D012F1140F0ED012F1100F71 +:101E300011D10AE012F1080F07D012F1040F04D0FB +:101E40004AB902E0D31E052B05D8012806D0022886 +:101E500008D003280AD0122528467CBD1046FDF77D +:101E600013F8F9E710460CF06BFEF5E70846144648 +:101E70006946FFF751FB08B10225EDE79DF8000028 +:101E80000198002580F86740E6E710B51346012267 +:101E9000FEF7EAFF002010BD10B5044610F02FFA3F +:101EA000052804D020460FF029FC002010BD0C208E +:101EB00010BD10B5044601F09DF808B10C2010BD0E +:101EC0002146002007F037FB002010BD10B5044666 +:101ED0000FF0A3FC50B108F0A6FD38B1207808F04F +:101EE00029FB20780DF04DF9002010BD0C2010BD0D +:101EF00010B5044601F07EF808B10C2010BD214653 +:101F0000012007F018FB002010BD38B504464FF63D +:101F1000FF70ADF80000A079E179884216D02079F1 +:101F2000FCF7E3FA90B16079FCF7DFFA70B10022B8 +:101F3000A079114612F0A0FD40B90022E0791146C7 +:101F400012F09AFD10B9207A072801D9122038BD65 +:101F500008F076FD60B910F0D2F948B90021684662 +:101F6000FFF78EFB20B1204606F044F9002038BD73 +:101F70000C2038BD2DE9FC41817805461A2925D071 +:101F80000EDC16292ED2DFE801F02D2D2D2D2D216E +:101F90002D2D2D2D2D2D2D2D2D2D2D2D2D21212195 +:101FA0002A291FD00BDCA1F11E010C291AD2DFE86F +:101FB00001F019191919191919191919190D3A399D +:101FC00004290FD2DFE801F00E020E022888B0F5D6 +:101FD000706F07D201276946FFF79EFA20B10220F1 +:101FE000BDE8FC811220FBE79DF8000000F0D2FF65 +:101FF000019C10B104F58A7401E004F5C6749DF8E3 +:10200000000000F0C7FF019E10B106F2151601E0B6 +:1020100006F28D166846FFF76DFA08B1207838B1E0 +:102020000C20DDE70C620200180800207800002078 +:102030002770A8783070684601F030F80020CFE7AC +:102040007CB50D466946FFF767FA002618B12E6089 +:102050002E7102207CBD9DF8000000F09BFF019CCA +:102060009DF80000703400F095FF019884F84260FC +:1020700081682960017B297194F842100029F5D10B +:1020800000207CBD10B5044600F0B4FF20B10BF079 +:1020900021FC08B10C2010BD207800F074FFE2791B +:1020A000611C0BF093FD08B1002010BD022010BD93 +:1020B00010B5886E60B1002241F8682F0120CA7106 +:1020C0008979884012F0CCFC002800D01F2010BD78 +:1020D0000C2010BD1CB50C466946FFF71DFA002800 +:1020E00009D19DF8000000280198B0F8700000D0D8 +:1020F000401C208000201CBD1CB504460088694699 +:10210000FFF70AFA08B102201CBD606828B1DDE9BA +:102110000001224601F04CF81CBDDDE90001FFF78B +:10212000C7FF1CBD70B51C460D4618B1012801D073 +:10213000122070BD1946104601F078F830B12146E2 +:10214000284601F07DF808B1002070BD302070BD38 +:1021500070B5044600780E46012804D018B1022854 +:1021600001D0032840D1607828B1012803D002288B +:1021700001D0032838D1E07B10B9A078012833D1F1 +:10218000A07830F005012FD110F0050F2CD0628916 +:10219000E188E0783346FFF7C5FF002825D1A07815 +:1021A00005281DD16589A289218920793346FFF749 +:1021B000B9FF002819D1012004EB40014A891544D8 +:1021C0002218D378927893420ED1CA8889888A429D +:1021D0000AD1401CC0B20228EED3E088A84203D343 +:1021E000A07B08B1072801D9122070BD002070BD66 +:1021F00010B586B0044600F0E1FE10B10C2006B028 +:1022000010BD022104F10A0001F02FF8A0788DF82A +:102210000800A0788DF8000060788DF80400207820 +:102220008DF80300A07B8DF80500E07B00B1012054 +:102230008DF80600A078C10717D0E07801F00CF8FF +:102240008DF80100E088ADF80A006089ADF80C0057 +:10225000A078400716D5207900F0FEFF8DF8020027 +:102260002089ADF80E00A0890AE040070AD5E07881 +:1022700000F0F2FF8DF80200E088ADF80E006089F2 +:10228000ADF8100002A80FF0D4FA0028B7D16846C4 +:102290000CF07CFFB3E710B504460121FFF758FFAF +:1022A000002803D12046BDE81040A1E710BD027808 +:1022B000012A01D0BAB118E042783AB1012A05D01A +:1022C000022A12D189B1818879B100E059B14188DF +:1022D00049B1808838B101EB8101490000EB8000F1 +:1022E000B1EB002F01D2002070471220704770B56B +:1022F000044600780D46012809D010F000F80528A2 +:1023000003D00FF0A6F9002800D00C2070BD0CF00F +:102310000AFE88B10CF01CFE0CF018FF0028F5D165 +:1023200025B160780CF0ACFE0028EFD1A188608860 +:10233000BDE870400FF0A3BA122070BD10B504467E +:102340000121FFF7B4FF002804D12046BDE810406A +:102350000121CCE710BDF0B5871FDDE9056540F62A +:102360007B44A74213D28F1FA74210D288420ED8B7 +:10237000B2F5FA7F0BD2A3F10A00241FA04206D2C5 +:10238000521C4A43B2EB830F01DAAE4201D900205E +:10239000F0BD0120F0BD2DE9FC47477A894604468F +:1023A00017F0050F7ED0F8087CD194F83A0008B9F0 +:1023B000012F77D10025A8462E46F90789F0010A9A +:1023C00019D0208A514600F031FFE8B360895146A8 +:1023D00000F036FFC0B3208A6189884262D8A18E9E +:1023E000E08DCDE90001238D628CA18BE08AFFF79F +:1023F000B2FF48B30125B8070ED504EB4500828E25 +:10240000C18DCDE90012038D428C818BC08AFFF70C +:10241000A2FFC8B1A8466D1C78071ED504EB45067F +:102420005146308A00F002FF70B17089514600F0C9 +:1024300007FF48B1308A7189884253D8B18EF08D38 +:10244000CDE90001338D00E00BE0728CB18BF08A96 +:10245000FFF781FF28B12E466D1CB9F1000F03D0A4 +:1024600030E03020BDE8FC87F80707D0780705D5B5 +:1024700004EB460160894989884233D1228A0121CF +:102480001BE0414503D004EB4100008A024404EB09 +:102490004100C38A868AB34224D1838B468BB342E0 +:1024A00020D100E01EE0438C068CB3421AD1038D8C +:1024B000C08C834216D1491CC9B2A942E1D36089BC +:1024C00090420FD3207810B101280BD102E0A07800 +:1024D0000028F9D1607838B1012805D0022803D04E +:1024E000032801D01220BDE70020BBE7002152E7FE +:1024F0000178C90702D0406811F0A9BE11F076BE7C +:1025000010B50078012800D00020FCF7B8FC0020AE +:1025100010BD2DE9F0478EB00D46AFF6A422D2E9EA +:102520000092014690462846FFF735FF06000CD181 +:1025300000F044FD40B9FE4F387828B90CF0B2F9EC +:10254000A0F57F41FF3903D00C200EB0BDE8F08725 +:10255000032105F1100000F088FEF54809AA3E3875 +:102560000990F4480A90F248062110380B900CA804 +:1025700001F06AFC040037D00021FEF77CF904F179 +:1025800030017B8ABA8ACB830A84797C0091BA466F +:102590003B7CBA8A798A208801F044FD00B1FFDFD4 +:1025A000208806F058FF218804F10E0000F02CFD71 +:1025B000E1A004F1120700680590032105A804F0CA +:1025C0006DFF002005A90A5C3A54401CC0B20328E4 +:1025D000F9D3A88B6080688CA080288DE080687A11 +:1025E000410703D508270AE00920AEE7C10701D05B +:1025F000012704E0800701D5022700E000273A46C2 +:10260000BAF8160011460FF0CFF90146A062204635 +:102610000FF0D8F93A4621460020FEF7AEFD00B98A +:102620000926C34A21461C320020FEF7C3FD0027BD +:1026300084F8767084F87770A87800F0A4FC60764F +:10264000D5F80300C4F81A00B5F80700E083C4F811 +:10265000089084F80C80012084F8200101468DF850 +:102660000070684604F01AFF9DF8000000F00701B2 +:10267000C0F3C1021144C0F3401008448DF80000BB +:10268000401D2076092801D20830207601212046FD +:10269000FEF7F1F868780CF051FCEEBBA9782878C9 +:1026A000EA1C0CF01EFC48B10CF052FCA97828780A +:1026B000EA1C0CF0BFFC060002D052E0122650E0EB +:1026C000687A00F005010020CA0700D001208A07BF +:1026D00001D540F00200490701D540F008000CF098 +:1026E000E9FB06003DD1214603200CF0CDFC06009D +:1026F00037D10CF0D2FC060033D1697A01F0050124 +:102700008DF80810697AC90708D06889ADF80A0001 +:10271000288AADF80C0000E023E00120697A8A07DE +:1027200000D5401C490707D505EB40004189ADF8AD +:102730000E10008AADF8100002A80FF07AF80646D5 +:1027400095F83A0000B101200CF0C6FB4EB90CF030 +:10275000FDFC060005D1A98F20460FF00BF80600FE +:1027600008D0208806F078FE2088062101F0B0FB12 +:1027700000B1FFDF3046E8E601460020C9E638B583 +:102780006B48007878B90FF0BAFD052805D00CF039 +:1027900089F8A0F57F41FF3905D068460FF0B3F8FE +:1027A000040002D00CE00C2038BD0098008806F030 +:1027B00053FE00980621008801F08AFB00B1FFDF7C +:1027C000204638BD1CB582894189CDE900120389B4 +:1027D000C28881884088FFF7BEFD08B100201CBD7B +:1027E00030201CBD70B50546FFF7ECFF00280ED168 +:1027F0002888062101F05AFB040007D000F042FCB3 +:1028000020B1D4F81801017831B901E0022070BD7F +:10281000D4F86411097809B13A2070BD052181719D +:10282000D4F8181100200881D4F81811A88848811C +:10283000D4F81811E8888881D4F818112889C8813B +:10284000D4F81801028941898A4204D88279082A79 +:1028500001D88A4201D3122070BD29884180D4F862 +:10286000181102200870002070BD3EB50446FEF726 +:1028700075FAB0B12E480125A0F1400245702368D9 +:1028800042F8423F237900211371417069460620C6 +:1028900001F095FA00B1FFDF684601F06EFA10B161 +:1028A0000EE012203EBDBDF80440029880F8205191 +:1028B000684601F062FA18B9BDF80400A042F4D1EC +:1028C00000203EBD70B505460088062101F0EEFAF5 +:1028D000040007D000F0D6FB20B1D4F81811087816 +:1028E00030B901E0022070BDD4F86401007808B16D +:1028F0003A2070BDB020005D10F0010F22D0D5F855 +:1029000002004860D5F806008860D4F8180169898B +:1029100010228181D4F8180105F10C010E3004F564 +:102920008C74FBF78AFD216803200870288805E075 +:1029300018080020840000201122330021684880FC +:10294000002070BD0C2070BD38B504460078EF281B +:102950004DD86088ADF80000009800F097FC88B36F +:102960006188080708D4D4E9012082423FD8202A90 +:102970003DD3B0F5804F3AD8207B18B3072836D81E +:10298000607B28B1012803D0022801D003282ED172 +:102990004A0703D4022801D0032805D1A07B08B13F +:1029A000012824D1480707D4607D28B1012803D02D +:1029B000022801D003281AD1C806E07D03D50128DA +:1029C00015D110E013E0012801D003280FD1C8066B +:1029D00009D4607E012803D0022801D0032806D143 +:1029E000A07E0F2803D8E07E18B1012801D0122064 +:1029F00038BD002038BDF8B514460D46064608F02F +:102A00001FF808B10C20F8BD3046FFF79DFF0028E5 +:102A1000F9D1FCF73EFA2870B07554B9FF208DF853 +:102A2000000069460020FCF71EFA69460020FCF70A +:102A30000EFA3046BDE8F840FCF752B90022DAE75A +:102A40000078C10801D012207047FA4981F82000AF +:102A50000020704710B504460078C00704D1608894 +:102A600010B1FCF7D7F980B12078618800F001023D +:102A7000607800F02FFC002806D1FCF7B3F901467E +:102A80006088884203D9072010BD122010BD6168FC +:102A9000FCF7E9F9002010BD10B504460078C00726 +:102AA00004D1608810B1FBF78AFE70B1207861888C +:102AB00000F00102607800F00DFC002804D160886D +:102AC0006168FCF7C4F9002010BD122010BD7CB570 +:102AD000044640784225012808D8A078FBF767FE15 +:102AE00020B120781225012802D090B128467CBD63 +:102AF000FCF7DBF920B1A0880028F7D08028F5D8B2 +:102B0000FCF7DAF960B160780028EFD0207801286E +:102B100008D006F0C3FD044607F05DFC00287FD016 +:102B20000C207CBDFBF7F5FF10B9FCF7B7F990B3AB +:102B300007F086FF0028F3D1FBF700FEA0F57F41E8 +:102B4000FF39EDD1FCF707F8A68842F21070464332 +:102B5000A079FCF770F9FBF739FEF8B100220721E4 +:102B600001A801F071F9040058D0B3480021846035 +:102B70002046FDF72DFD2046FCF732FDAD4D04F15A +:102B800030006A8AA98AC2830184FBF726FE60B1FD +:102B9000E88A01210DE0FFE712207CBD31460020CC +:102BA00007F0CBFC88B3FFDF44E0FCF787F9014670 +:102BB000E88A07F091FD0146A0620022204606F057 +:102BC00070FDFBF70AFE38B9FCF778F9024621469A +:102BD0000120FEF7D2FAD0B1964A21461C320120DC +:102BE000FEF7E8FA687C00902B7CAA8A698A208824 +:102BF00001F018FA00B1FFDF208806F02CFC314606 +:102C0000204607F09AFC00B1FFDF13E008E007213F +:102C1000BDF8040001F05CF900B1FFDF09207CBDC4 +:102C200044B1208806F018FC2088072101F050F9F3 +:102C300000B1FFDF00207CBD002148E770B50D46E4 +:102C4000072101F033F9040003D094F88F0110B18B +:102C50000AE0022070BD94F87D00142801D01528E8 +:102C600002D194F8DC0108B10C2070BD1022294675 +:102C700004F5C870FBF7E1FB012084F88F01002008 +:102C800070BD10B5072101F011F918B190F88F113E +:102C900011B107E0022010BD90F87D10142903D077 +:102CA000152901D00C2010BD022180F88F110020C1 +:102CB00010BD2DE9FC410C464BF6803212219442A6 +:102CC0001DD8E4B16946FEF727FC002815D19DF810 +:102CD000000000F05FF9019E9DF80000703600F0E2 +:102CE00059F9019DAD1C2F88224639463046FDF723 +:102CF00065FC2888B842F6D10020BDE8FC81084672 +:102D0000FBE77CB5044600886946FEF705FC002811 +:102D100010D19DF8000000F03DF9019D9DF80000E4 +:102D2000703500F037F90198A27890F82C10914294 +:102D300001D10C207CBD7F212972A9720021E9728A +:102D4000E17880F82D10217980F82E10A17880F894 +:102D50002C1000207CBD1CB50C466946FEF7DCFB40 +:102D600000280AD19DF8000000F014F9019890F8AD +:102D70008C0000B10120207000201CBD7CB50D46E8 +:102D800014466946FEF7C8FB002809D19DF80000EB +:102D900000F000F9019890F82C00012801D00C20D7 +:102DA0007CBD9DF8000000F0F5F8019890F87810CF +:102DB000297090F87900207000207CBD70B50D4618 +:102DC0001646072101F072F818B381880124C388E0 +:102DD000428804EB4104AC4217D842F210746343BA +:102DE000A4106243B3FBF2F2521E94B24FF4FA7293 +:102DF000944200D91446A54200D22C46491C641CBA +:102E0000B4FBF1F24A43521E91B290F8C8211AB9AC +:102E100001E0022070BD01843180002070BD10B53A +:102E20000C46072101F042F840B1022C08D91220CB +:102E300010BD000018080020780000200220F7E7ED +:102E400014F0010180F8FD10C4F3400280F8FC206A +:102E500004D090F8FA1009B107F054FC0020E7E71D +:102E6000017889B1417879B141881B290CD38188D7 +:102E70001B2909D3C188022906D3F64902680A65CD +:102E800040684865002070471220704710B504461E +:102E90000EF086FD204607F0D8FB0020C8E710B5ED +:102EA00007F0D6FB0020C3E72DE9F04115460F4699 +:102EB00006460122114638460EF076FD04460121F1 +:102EC000384607F009FC844200D20446012130460E +:102ED00000F065F806460121002000F060F8311886 +:102EE000012096318C4206D901F19600611AB1FB9E +:102EF000F0F0401C80B228800020BDE8F08110B5C1 +:102F0000044600F077F808B10C2091E7601C0AF045 +:102F100038FE207800F00100FBF718FE207800F062 +:102F200001000CF010F8002082E710B504460720DD +:102F300000F056FF08B10C207AE72078C00711D0C6 +:102F400000226078114611F097FD08B112206FE75A +:102F5000A06809F01DFB6078D4F8041009F021FB8B +:102F6000002065E7002009F013FB00210846F5E783 +:102F700010B505F036FE00205AE710B5006805F0E0 +:102F800084F8002054E718B1022801D001207047CE +:102F90000020704708B1002070470120704710B52D +:102FA000012904D0022905D0FFDF204640E7C000F8 +:102FB000503001E080002C3084B2F6E710B50FF0FD +:102FC0009EF9042803D0052801D0002030E7012015 +:102FD0002EE710B5FFF7F2FF10B10CF07BF828B91F +:102FE00007F02EFD20B1FBF78CFD08B101201FE793 +:102FF00000201DE710B5FFF7E1FF18B907F020FD2D +:10300000002800D0012013E72DE9FE4300250F46DC +:1030100080460A260421404604F069FA4046FDF73E +:103020003EFE062000F0EAFE044616E06946062051 +:1030300000F0C5FE0BE000BFBDF80400B84206D0AA +:103040000298042241460E30FBF7CAF950B1684697 +:1030500000F093FE0500EFD0641E002C06DD002D6D +:10306000E4D005E04046FDF723FEF5E705B9FFDFB4 +:10307000D8F80000FDF737FE761E01D00028C9D031 +:10308000BDE8FE8390F8F01090F88C0020B919B1DB +:10309000042901D0012070470020704701780029E1 +:1030A0000AD0416891F8FA20002A05D0002281F860 +:1030B000FA20406807F026BB704770B514460546F5 +:1030C000012200F01BF9002806D121462846BDE860 +:1030D0007040002200F012B970BDFB2802D8B1F593 +:1030E000296F01D911207047002070471B38E12853 +:1030F00006D2B1F5A47F03D344F29020814201D9D6 +:1031000012207047002070471FB55249403191F896 +:103110002010CA0702D102781D2A0AD08A0702D4D9 +:1031200002781C2A28D049073DD40178152937D0C8 +:1031300039E08088ADF8000002A9FEF7EDF900B192 +:10314000FFDF9DF80800FFF725FF039810F8601FC8 +:103150008DF8021040788DF803000020ADF80400CF +:1031600001B9FFDF9DF8030000B9FFDF6846FEF7F5 +:1031700040FCD8B1FFDF19E08088ADF800004FF4C3 +:103180002961FB20ADF80410ADF80200ADF806008F +:10319000ADF808106846FEF73AFD38B1FFDF05E0EC +:1031A000807BC00702D0002004B041E60120FBE78D +:1031B000F8B50746508915460C4640B1B0F5004FAA +:1031C00005D20022A878114611F056FC08B1122051 +:1031D000F8BDA06E04F1700630B1A97894F86E00C5 +:1031E000814201D00C20F8BD012184F86F10A9782C +:1031F00084F86E106968A1666989A4F86C10288942 +:10320000B084002184F86F1028886946FEF762FFB9 +:10321000B08CBDF80010081A00B2002804DD214669 +:103220003846FEF745FFDDE70020F8BD042803D34C +:1032300021B9B0F5804F01D90020704701207047B7 +:10324000042803D321B9B0F5804F01D9002070477D +:1032500001207047D8070020012802D018B10020B3 +:103260007047022070470120704710B500224FF4CC +:10327000C84408E030F81230A34200D9234620F8B1 +:103280001230521CD2B28A42F4D3D1E580B2C106C8 +:103290000BD401071CD481064FEAC07101D5B9B91E +:1032A00000E099B1800713D410E0410610D48106E4 +:1032B0000ED4C1074FEA807104D0002902DB400719 +:1032C00004D405E0010703D4400701D4012070476E +:1032D0000020704770B50C460546FF2904D8FBF75F +:1032E0007CFA18B11F2C01D9122070BD2846FBF7BB +:1032F0005EFA08B1002070BD422070BD0AB1012203 +:1033000000E00222024202D1C80802D109B1002025 +:10331000704711207047000030B5058825F400443F +:1033200021448CB24FF4004194420AD2121B92B253 +:103330001B339A4201D2A94307E005F4004121431F +:1033400003E0A21A92B2A9431143018030BD0844A0 +:10335000083050434A31084480B2704770B51D466A +:1033600016460B46044629463046049AFFF7EFFFFF +:103370000646B34200D2FFDF282200212046FBF799 +:1033800086F84FF6FF70A082283EB0B26577608065 +:10339000B0F5004F00D9FFDF618805F13C008142A4 +:1033A00000D2FFDF60880835401B343880B22080AF +:1033B0001B2800D21B2020800020A07770BD8161D7 +:1033C000886170472DE9F05F0D46C188044600F121 +:1033D0002809008921F4004620F4004800F063FB2E +:1033E00010B10020BDE8F09F4FF0000A4FF0010B34 +:1033F000B0450CD9617FA8EB0600401A0838854219 +:1034000019DC09EB06000021058041801AE0608884 +:10341000617F801B471A083F0DD41B2F00DAFFDFA6 +:10342000BD4201DC294600E0B9B2681A0204120C60 +:1034300004D0424502DD84F817A0D2E709EB06006C +:103440000180428084F817B0CCE770B5044600F1E3 +:103450002802C088E37D20F400402BB1104402888C +:10346000438813448B4201D2002070BD00258A425C +:1034700002D30180458008E0891A0904090C4180C3 +:1034800003D0A01D00F01FFB08E0637F0088083315 +:10349000184481B26288A01DFFF73EFFE575012048 +:1034A00070BD70B5034600F12804C588808820F4FB +:1034B00000462644A84202D10020188270BD988997 +:1034C0003588A84206D3401B75882D1A2044ADB21A +:1034D000C01E05E02C1AA5B25C7F20443044401D7C +:1034E0000C88AC4200D90D809C8924B10024147052 +:1034F0000988198270BD0124F9E770B5044600F10E +:103500002801808820F400404518208A002825D012 +:10351000A189084480B2A08129886A881144814227 +:1035200000D2FFDF2888698800260844A1898842E4 +:1035300012D1A069807F2871698819B1201D00F01F +:10354000C2FA08E0637F28880833184481B2628891 +:10355000201DFFF7E1FEA6812682012070BD2DE926 +:10356000F041418987880026044600F12805B942C8 +:1035700019D004F10A0800BF21F400402844418812 +:1035800019B1404600F09FFA08E0637F00880833D5 +:10359000184481B262884046FFF7BEFE761C6189FE +:1035A000B6B2B942E8D13046BDE8F0812DE9F0412C +:1035B00004460B4627892830A68827F40041B4F832 +:1035C0000A8001440D46B74201D10020ECE70AB160 +:1035D000481D106023B1627F691D1846FAF72DFF60 +:1035E0002E88698804F1080021B18A1996B200F08A +:1035F0006AFA06E0637F62880833991989B2FFF797 +:103600008BFE474501D1208960813046CCE7818817 +:10361000C088814201D10120704700207047018994 +:103620008088814201D1012070470020704770B529 +:103630008588C38800F1280425F4004223F4004162 +:1036400014449D421AD08389058A5E1925886388AF +:10365000EC18A64214D313B18B4211D30EE0437F72 +:1036600008325C192244408892B2801A80B2233317 +:10367000984201D211B103E08A4201D1002070BD0D +:10368000012070BD2DE9F0478846C18804460089B5 +:1036900021F4004604F1280720F4004507EB060951 +:1036A00000F001FA002178BBB54204D9627FA81B63 +:1036B000801A002503E06088627F801B801A08382A +:1036C00023D4E28962B1B9F80020B9F802303BB1E5 +:1036D000E81A2177404518DBE0893844801A09E070 +:1036E000801A217740450ADB607FE1890830304449 +:1036F00039440844C01EA4F81280BDE8F08745454F +:1037000003DB01202077E7E7FFE761820020F4E791 +:103710002DE9F74F044600F12805C088884620F4BB +:10372000004A608A05EB0A0608B1404502D2002033 +:10373000BDE8FE8FE08978B13788B6F8029007EBD4 +:103740000901884200D0FFDF207F4FF0000B50EAD4 +:10375000090106D088B33BE00027A07FB94630714D +:10376000F2E7E18959B1607F2944083050440844A8 +:10377000B4F81F1020F8031D94F821108170E2891D +:1037800007EB080002EB0801E1813080A6F802B0E7 +:1037900002985F4650B1637F30880833184481B285 +:1037A0006288A01DFFF7B8FDE78121E0607FE18915 +:1037B00008305044294408442DE0FFE7E089B4F87C +:1037C0001F102844C01B20F8031D94F8211081709D +:1037D00009EB0800E28981B202EB0800E081378042 +:1037E00071800298A0B1A01D00F06DF9A4F80EB090 +:1037F000A07F401CA077A07D08B1E088A08284F85B +:1038000016B000BFA4F812B084F817B001208FE7FB +:10381000E0892844C01B30F8031DA4F81F108078ED +:1038200084F82100EEE710B5818800F1280321F427 +:1038300000442344848AC288A14212D0914210D00D +:10384000818971B9826972B11046FFF7E8FE50B9FB +:103850001089283220F400401044197900798842F8 +:1038600001D1002010BD184610BD00F12803407F93 +:1038700008300844C01E1060088808B9DB1E1360B9 +:1038800008884988084480B270472DE9F04100F16A +:103890002806407F1C4608309046431808884D880B +:1038A000069ADB1EA0B1C01C80B2904214D9801AC7 +:1038B000A04200DB204687B298183A464146FAF704 +:1038C0008FFD002816D1E01B84B2B844002005E02B +:1038D000ED1CADB2F61EE8E7101A80B20119A9423C +:1038E00006D8304422464146BDE8F041FAF778BD9B +:1038F0004FF0FF3058E62DE9F04100F12804407FF9 +:103900001E46083090464318002508884F88069ABE +:10391000DB1E90B1C01C80B2904212D9801AB04216 +:1039200000DB304685B299182A464046FAF785FDF5 +:10393000701B86B2A844002005E0FF1CBFB2E41E45 +:10394000EAE7101A80B28119B94206D82118324626 +:103950004046FAF772FDA81985B2284624E62DE9FB +:10396000F04100F12804407F1E460830904643187D +:10397000002508884F88069ADB1E90B1C01C80B2D3 +:10398000904212D9801AB04200DB304685B29818B6 +:103990002A464146FAF751FD701B86B2A844002022 +:1039A00005E0FF1CBFB2E41EEAE7101A80B28119DD +:1039B000B94206D8204432464146FAF73EFDA819DE +:1039C00085B22846F0E5401D704710B5044600F169 +:1039D0002801C288808820F400431944904206D010 +:1039E000A28922B9228A12B9A28A904201D100206A +:1039F00010BD0888498831B1201D00F064F800200E +:103A00002082012010BD637F62880833184481B290 +:103A1000201DFFF781FCF2E70021C181017741827F +:103A2000C1758175704703881380C28942B1C2880D +:103A300022F4004300F128021A440A60C08970474A +:103A40000020704710B50446808AA0F57F41FF39F9 +:103A500000D0FFDFE088A082E08900B10120A075DE +:103A600010BD4FF6FF71818200218175704710B53E +:103A70000446808AA0F57F41FF3900D1FFDFA07D99 +:103A800028B9A088A18A884201D1002010BD012058 +:103A900010BD8188828A914201D1807D08B10020C9 +:103AA00070470120704720F4004221F400439A42FD +:103AB00007D100F4004001F40041884201D0012008 +:103AC00070470020704730B5044600880D4620F44A +:103AD0000040A84200D2FFDF21884FF40040884315 +:103AE0002843208030BD70B50C00054609D0082C55 +:103AF00000D2FFDF1DB1A1B2286800F044F8201DFC +:103B000070BD0DB100202860002070BD002102684A +:103B100003E093881268194489B2002AF9D100F0B1 +:103B200032B870B500260D460446082900D2FFDFE2 +:103B3000206808B91EE0044620688188A94202D0A6 +:103B400001680029F7D181880646A94201D10068A1 +:103B50000DE005F1080293B20022994209D32844EE +:103B6000491B026081802168096821600160206032 +:103B700000E00026304670BD00230B608A8002689A +:103B80000A600160704700234360021D01810260EA +:103B90007047F0B50F460188408815460C181E4640 +:103BA000AC4200D3641B3044A84200D9FFDFA01907 +:103BB000A84200D9FFDF3819F0BD2DE9F041884651 +:103BC00006460188408815460C181F46AC4200D3B3 +:103BD000641B3844A84200D9FFDFE019A84200D98D +:103BE000FFDF70883844708008EB0400BDE8F08186 +:103BF0002DE9F041054600881E461746841B88467D +:103C0000BC4200D33C442C8068883044B84200D980 +:103C1000FFDFA019B84200D9FFDF68883044688010 +:103C200008EB0400E2E72DE9F04106881D46044652 +:103C3000701980B2174688462080B84201D3C01B55 +:103C400020806088A84200D2FFDF7019B84200D9F6 +:103C5000FFDF6088401B608008EB0600C6E730B5D8 +:103C60000D460188CC18944200D3A41A408898428B +:103C700000D8FFDF281930BD2DE9F041C84D0446BA +:103C80009046A8780E46A04200D8FFDF05EB8607D5 +:103C9000B86A50F8240000B1FFDFB868002816D0D9 +:103CA000304600F044F90146B868FFF73AFF0500D6 +:103CB0000CD0B86A082E40F8245000D3FFDFB94872 +:103CC0004246294650F82630204698472846BDE807 +:103CD000F0812DE9F8431E468C1991460F460546A2 +:103CE000FF2C00D9FFDFB14500D9FFDFE4B200951A +:103CF0004DB300208046E81C20F00300A84200D00D +:103D0000FFDF4946DFF89892684689F8001089F885 +:103D1000017089F8024089F8034089F8044089F865 +:103D2000054089F8066089F80770414600F008F9F7 +:103D3000002142460F464B460098C01C20F003006D +:103D4000009012B10EE00120D4E703EB8106B062CF +:103D5000002005E0D6F828C04CF82070401CC0B206 +:103D6000A042F7D30098491C00EB8400C9B2009030 +:103D70000829E1D3401BBDE8F88310B50446EDF7F0 +:103D80008EFA08B1102010BD2078854A618802EBB8 +:103D9000800092780EE0836A53F8213043B14A1CC8 +:103DA0006280A180806A50F82100A060002010BDD0 +:103DB000491C89B28A42EED86180052010BD70B5D9 +:103DC00005460C460846EDF76AFA08B1102070BDAA +:103DD000082D01D3072070BD25700020608070BDC4 +:103DE0000EB56946FFF7EBFF00B1FFDF6846FFF74E +:103DF000C4FF08B100200EBD01200EBD10B5044661 +:103E0000082800D3FFDF6648005D10BD3EB50546BB +:103E100000246946FFF7D3FF18B1FFDF01E0641CFF +:103E2000E4B26846FFF7A9FF0028F8D02846FFF75C +:103E3000E5FF001BC0B23EBD59498978814201D9D6 +:103E4000C0B27047FF2070472DE9F041544B06295E +:103E500003D007291CD19D7900E0002500244FF6EE +:103E6000FF7603EB810713F801C00AE06319D7F866 +:103E700028E09BB25EF823E0BEF1000F04D0641C82 +:103E8000A4B2A445F2D8334603801846B34201D108 +:103E900000201CE7BDE8F041EEE6A0F57F43FF3BC4 +:103EA00001D0082901D300207047E5E6A0F57F4244 +:103EB000FF3A0BD0082909D2394A9378834205D9B1 +:103EC00002EB8101896A51F8200070470020704799 +:103ED0002DE9F04104460D46A4F57F4143F202006E +:103EE000FF3902D0082D01D30720F0E62C494FF00E +:103EF00000088A78A242F8D901EB8506B26A52F826 +:103F00002470002FF1D027483946203050F8252062 +:103F100020469047B16A284641F8248000F007F80F +:103F200002463946B068FFF727FE0020CFE61D495C +:103F3000403131F810004FF6FC71C01C084070474A +:103F40002DE9F843164E8846054600242868C01C13 +:103F500020F0030028602046FFF7E9FF315D484369 +:103F6000B8F1000F01D0002200E02A68014600925B +:103F700032B100274FEA0D00FFF7B5FD1FB106E093 +:103F800001270020F8E706EB8401009A8A6029687F +:103F9000641C0844E4B22860082CD7D3EBE6000088 +:103FA0003C0800201862020070B50E461D461146FE +:103FB00000F0D3F804462946304600F0D7F82044F4 +:103FC000001D70BD2DE9F04190460D4604004FF0F4 +:103FD000000610D00027E01C20F00300A04200D013 +:103FE000FFDFE5B141460020FFF77DFD0C3000EB1F +:103FF000850617B113E00127EDE7614F04F10C00CE +:10400000AA003C602572606000EB85002060002102 +:104010006068FAF73CFA41463868FFF764FD3046BD +:10402000BDE8F0812DE9FF4F554C804681B02068F6 +:104030009A46934600B9FFDF2068027A424503D9C9 +:10404000416851F8280020B143F2020005B0BDE8F4 +:10405000F08F5146029800F080F886B258460E99CB +:1040600000F084F885B27019001D87B22068A1465F +:1040700039460068FFF755FD04001FD06780258092 +:104080002946201D0E9D07465A4601230095FFF73D +:1040900065F92088314638440123029ACDF800A002 +:1040A000FFF75CF92088C1193846FFF788F9D9F87D +:1040B00000004168002041F82840C7E70420C5E718 +:1040C00070B52F4C0546206800B9FFDF2068017AE3 +:1040D000A9420DD9426852F8251049B1002342F88F +:1040E00025304A880068FFF747FD2168087A06E016 +:1040F00043F2020070BD4A6852F820202AB9401EDF +:10410000C0B2F8D20868FFF701FD002070BD70B59D +:104110001B4E05460024306800B9FFDF3068017A85 +:10412000A94204D9406850F8250000B1041D20467A +:1041300070BD70B5124E05460024306800B9FFDF2F +:104140003068017AA94206D9406850F8251011B1AB +:1041500031F8040B4418204670BD10B50A46012101 +:10416000FFF7F5F8C01C20F0030010BD10B50A469B +:104170000121FFF7ECF8C01C20F0030010BD000087 +:104180008C00002070B50446C2F110052819FAF71A +:1041900054F915F0FF0109D0491ECAB28020A0547D +:1041A0002046BDE870400021FAF771B970BD30B506 +:1041B00005E05B1EDBB2CC5CD55C6C40C454002BCC +:1041C000F7D130BD10B5002409E00B78521E44EA47 +:1041D000430300F8013B11F8013BD2B2DC09002A8D +:1041E000F3D110BD2DE9F04389B01E46DDE9107909 +:1041F00090460D00044622D002460846F949FDF7D4 +:1042000044FE102221463846FFF7DCFFE07B000623 +:1042100006D5F44A3946102310320846FFF7C7FF87 +:10422000102239464846FFF7CDFFF87B000606D539 +:10423000EC4A4946102310320846FFF7B8FF102217 +:1042400000212046FAF723F90DE0103EB6B208EB44 +:104250000601102322466846FFF7A9FF224628469A +:104260006946FDF712FE102EEFD818D0F2B2414683 +:104270006846FFF787FF10234A46694604A8FFF700 +:1042800096FF1023224604A96846FFF790FF2246B6 +:1042900028466946FDF7F9FD09B0BDE8F083102313 +:1042A0003A464146EAE770B59CB01E4605461346BD +:1042B00020980C468DF80800202219460DF10900BF +:1042C000FAF7BBF8202221460DF12900FAF7B5F8DC +:1042D00017A913A8CDE90001412302AA31462846B7 +:1042E000FFF780FF1CB070BD2DE9FF4F9FB014AEEB +:1042F000DDE92D5410AFBB49CDE9007620232031F4 +:104300001AA8FFF76FFF4FF000088DF808804FF0F4 +:1043100001098DF8099054F8010FCDF80A00A08822 +:10432000ADF80E0014F8010C1022C0F340008DF817 +:10433000100055F8010FCDF81100A888ADF8150050 +:1043400015F8010C2C99C0F340008DF8170006A851 +:104350008246FAF772F80AA8834610222299FAF7E1 +:104360006CF8A0483523083802AA40688DF83C80D4 +:10437000CDE900760E901AA91F98FFF733FF8DF84C +:1043800008808DF809902068CDF80A00A088ADF863 +:104390000E0014F8010C1022C0F340008DF810003C +:1043A0002868CDF81100A888ADF8150015F8010CA3 +:1043B0002C99C0F340008DF817005046FAF73DF8ED +:1043C000584610222299FAF738F8864835230838DB +:1043D00002AA40688DF83C90CDE900760E901AA9AB +:1043E0002098FFF7FFFE23B0BDE8F08FF0B59BB03B +:1043F0000C460546DDE922101E461746DDE920324F +:10440000D0F801C0CDF808C0B0F805C0ADF80CC0B8 +:104410000078C0F340008DF80E00D1F80100CDF80F +:104420000F00B1F80500ADF8130008781946C0F385 +:1044300040008DF815001088ADF8160090788DF8C2 +:1044400018000DF119001022F9F7F7FF0DF12900FE +:1044500010223146F9F7F1FF0DF1390010223946EB +:10446000F9F7EBFF17A913A8CDE90001412302AA30 +:1044700021462846FFF7B6FE1BB0F0BDF0B5A3B04D +:1044800017460D4604461E46102202A82899F9F741 +:10449000D4FF06A820223946F9F7CFFF0EA8202224 +:1044A0002946F9F7CAFF1EA91AA8CDE90001502331 +:1044B00002AA314616A8FFF795FE1698206023B091 +:1044C000F0BDF0B589B00446DDE90E070D46397838 +:1044D000109EC1F340018DF8001031789446C1F36D +:1044E00040018DF801101968CDF802109988ADF8D7 +:1044F000061099798DF808100168CDF809108188A7 +:10450000ADF80D1080798DF80F0010236A466146D2 +:1045100004A8FFF74CFE2246284604A9FDF7B5FC87 +:10452000D6F801000090B6F80500ADF80400D7F801 +:104530000100CDF80600B7F80500ADF80A0000202C +:10454000039010236A46214604A8FFF730FE224656 +:10455000284604A9FDF799FC09B0F0BD1FB51C68F9 +:1045600000945B68019313680293526803920246B9 +:1045700008466946FDF789FC1FBD10B588B00446A2 +:104580001068049050680590002006900790084637 +:104590006A4604A9FDF779FCBDF80000208008B048 +:1045A00010BD1FB51288ADF800201A88ADF80220A2 +:1045B0000022019202920392024608466946FDF7E4 +:1045C00064FC1FBD7FB5074B14460546083B9A1C8B +:1045D0006846FFF7E6FF224669462846FFF7CDFF0B +:1045E0007FBD00007062020070B5044600780E4680 +:1045F000012813D0052802D0092813D10EE0A068A5 +:1046000061690578042003F059FA052D0AD0782352 +:1046100000220420616903F0A7F903E00420616926 +:1046200003F04CFA31462046BDE8704001F08AB8EC +:1046300010B500F12D03C2799C78411D144064F33C +:104640000102C271D2070DD04A795C7922404A71C9 +:104650000A791B791A400A718278C9788A4200D98E +:10466000817010BD00224A71F5E74178012900D020 +:104670000C21017070472DE9F04F93B04FF0000B03 +:104680000C690D468DF820B0097801260C201746DC +:104690004FF00D084FF0110A4FF008091B2975D291 +:1046A000DFE811F01B00C40207031F035E03710360 +:1046B000A303B803F9031A0462049504A204EF04E7 +:1046C0002D05370555056005F305360639066806DC +:1046D0008406FE062207EB06F00614B120781D289A +:1046E0002AD0D5F808805FEA08004FD001208DF865 +:1046F0002000686A02220D908DF824200A208DF88F +:104700002500A8690A90A8880028EED098F8001023 +:1047100091B10F2910D27DD2DFE801F07C1349DE80 +:10472000FCFBFAF9F8F738089CF6F50002282DD1C1 +:1047300024B120780C2801D00026F0E38DF8202049 +:10474000CBE10420696A03F0B9F9A8880728EED103 +:10475000204600F0F2FF022809D0204600F0EDFFCD +:10476000032807D9204600F0E8FF072802D20120DD +:10477000207004E0002CB8D020780128D7D198F818 +:104780000400C11F0A2902D30A2061E0C4E1A0701D +:10479000D8F80010E162B8F80410218698F80600F5 +:1047A00084F83200012028700320207044E007289C +:1047B000BDD1002C99D020780D28B8D198F80310DD +:1047C00094F82F20C1F3C000C2F3C002104201D000 +:1047D000062000E00720890707D198F8051001425C +:1047E000D2D198F806100142CED194F8312098F831 +:1047F000051020EA02021142C6D194F8322098F83E +:10480000061090430142BFD198F80400C11F0A2945 +:10481000BAD200E008E2617D81427CD8D8F800106D +:104820006160B8F80410218198F80600A072012098 +:1048300028700E20207003208DF82000686A0D90EB +:1048400004F12D000990601D0A900F300B9022E1B9 +:104850002875FCE3412891D1204600F06EFF042822 +:1048600002D1E078C00704D1204600F066FF0F288F +:1048700084D1A88CD5F80C8080B24FF0400BE6694B +:10488000FFF745FC324641465B464E46CDF8009068 +:10489000FFF731F80B208DF82000686A0D90E06971 +:1048A0000990002108A8FFF79FFE2078042806D071 +:1048B000A07D58B1012809D003280AD04AE3052079 +:1048C0002070032028708DF82060CEE184F800A0CD +:1048D00032E712202070EAE11128BCD1204600F016 +:1048E0002CFF042802D1E078C00719D0204600F040 +:1048F00024FF062805D1E078C00711D1A07D022849 +:104900000ED0204608E0CCE084E072E151E124E1E1 +:1049100003E1E9E019E0B0E100F00FFF11289AD1BE +:10492000102208F1010104F13C00F9F786FD6078DE +:1049300001286ED012202070E078C00760D0A07DE2 +:104940000028C8D00128C6D05AE0112890D12046AE +:1049500000F0F3FE082804D0204600F0EEFE1328F5 +:1049600086D104F16C00102208F101010646F9F726 +:1049700064FD207808280DD014202070E178C80745 +:104980000DD0A07D02280AD06278022A04D0032824 +:10499000A1D035E00920F0E708B1012837D1C807D8 +:1049A00013D0A07D02281DD000200090D4E906215C +:1049B00033460EA8FFF777FC10220EA904F13C0045 +:1049C000F9F70EFDC8B1042042E7D4E90912201D11 +:1049D0008DE8070004F12C0332460EA8616BFFF747 +:1049E00070FDE9E7606BC1F34401491E0068C840EF +:1049F00000F0010040F08000D7E72078092806D1B8 +:104A000085F800908DF8209036E32870EFE30920B8 +:104A1000FBE79EE1112899D1204600F08EFE0A287E +:104A200002D1E078C00704D1204600F086FE1528A8 +:104A30008CD104F13C00102208F101010646F9F77F +:104A4000FCFC20780A2816D016202070D4E9093200 +:104A5000606B611D8DE80F0004F15C0304F16C02D2 +:104A600047310EA8FFF7C2FC10220EA93046F9F715 +:104A7000B7FC18B1F9E20B20207073E22046FFF773 +:104A8000D7FDA078216AC0F110020B18002118464A +:104A9000F9F7FDFC26E3394608A8FFF7A5FD064611 +:104AA0003CE20228B7D1204600F047FE042804D398 +:104AB000204600F042FE082809D3204600F03DFEC3 +:104AC0000E2829D3204600F038FE122824D2A07DDB +:104AD0000228A0D10E208DF82000686A0D9098F869 +:104AE00001008DF82400F5E3022894D1204600F05F +:104AF00024FE002810D0204600F01FFE0128F9D027 +:104B0000204600F01AFE0C28F4D004208DF8240072 +:104B100098F801008DF8250060E21128FCD1002CE6 +:104B2000FAD020781728F7D16178606A022912D06C +:104B30005FF0000101EB4101182606EBC1011022D4 +:104B4000405808F10101F9F778FC0420696A00F087 +:104B5000E7FD2670F0E50121ECE70B28DCD1002C05 +:104B6000DAD020781828D7D16078616A02281CD062 +:104B70005FF0000000EB4002102000EBC20009587B +:104B8000B8F8010008806078616A02280FD0002020 +:104B900000EB4002142000EBC2000958404650F8D8 +:104BA000032F0A604068486039E00120E2E70120F5 +:104BB000EEE71128B0D1002CAED020781928ABD167 +:104BC0006178606A022912D05FF0000101EB4101B7 +:104BD0001C2202EBC1011022405808F10101F9F733 +:104BE0002CFC0420696A00F09BFD1A20B6E001212C +:104BF000ECE7082890D1002C8ED020781A288BD191 +:104C0000606A98F80120017862F347010170616AD7 +:104C1000D8F8022041F8012FB8F806008880042057 +:104C2000696A00F07DFD90E2072011E638780128DE +:104C300094D1182204F114007968F9F7FEFBE079A9 +:104C4000C10894F82F0001EAD001E07861F3000078 +:104C5000E070217D002974D12178032909D0C00793 +:104C600025D0032028708DF82090686A0D9041208F +:104C700008E3607DA178884201D90620E8E5022694 +:104C80002671E179204621F0E001E171617A21F09D +:104C9000F0016172A17A21F0F001A172FFF7C8FC66 +:104CA0002E708DF82090686A0D900720EAE20420AB +:104CB000ABE6387805289DD18DF82000686A0D9004 +:104CC000B8680A900720ADF824000A988DF830B033 +:104CD0006168016021898180A17A8171042020703E +:104CE000F8E23978052985D18DF82010696A0D918F +:104CF000391D09AE0EC986E80E004121ADF8241019 +:104D00008DF830B01070A88CD7F80C8080B2402697 +:104D1000A769FFF70EFA41463A463346C846CDF832 +:104D20000090FEF71CFE002108A8FFF75DFCE0786C +:104D300020F03E00801CE0702078052802D00F2073 +:104D40000CE04AE1A07D20B1012802D0032802D066 +:104D500002E10720BEE584F80080EDE42070EBE47A +:104D6000102104F15C0002F0C2FB606BB0BBA07DBF +:104D700018B1012801D00520FDE006202870F84870 +:104D80006063A063C2E23878022894D1387908B110 +:104D90002875B7E3A07D022802D0032805D022E0C1 +:104DA000B8680028F5D060631CE06078012806D060 +:104DB000A07994F82E10012805D0E94806E0A179E1 +:104DC00094F82E00F7E7B8680028E2D06063E07836 +:104DD000C00701D0012902D0E14803E003E0F868F0 +:104DE0000028D6D0A06306200FE68DF82090696ACF +:104DF0000D91E1784846C90709D06178022903D1AD +:104E0000A17D29B1012903D0A17D032900D007206C +:104E1000287033E138780528BBD1207807281ED0C8 +:104E200084F800A005208DF82000686A0D90B8680D +:104E30000A90ADF824A08DF830B003210170E1781C +:104E4000CA070FD0A27D022A1AD000210091D4E90E +:104E5000061204F15C03401CFFF725FA6BE384F8AB +:104E60000090DFE7D4E90923211D8DE80E0004F14D +:104E70002C0304F15C02401C616BFFF722FB5AE338 +:104E8000626BC1F34401491E1268CA4002F001017D +:104E900041F08001DAE738780528BDD18DF820008F +:104EA000686A0D90B8680A90ADF824A08DF830B00B +:104EB000042100F8011B102204F15C01F9F7BDFA8E +:104EC000002108A8FFF790FB2078092801D01320C3 +:104ED00044E70A2020709AE5E078C10742D0A17D1E +:104EE000012902D0022927D038E0617808A80129D9 +:104EF00016D004F16C010091D4E9061204F15C03B0 +:104F0000001DFFF7BBFA0A20287003268DF82080C9 +:104F1000686A0D90002108A8FFF766FBE1E2C7E28E +:104F200004F15C010091D4E9062104F16C03001D39 +:104F3000FFF7A4FA0026E9E7C0F3440114290DD2D3 +:104F40004FF0006101EBB0104FEAB060E0706078A4 +:104F5000012801D01020BDE40620FFE6607801287A +:104F60003FF4B6AC0A2050E5E178C90708D0A17D2E +:104F7000012903D10B202870042030E028702EE096 +:104F80000E2028706078616B012818D004F15C0352 +:104F900004F16C020EA8FFF7E1FA2046FFF748FB88 +:104FA000A0780EAEC0F1100230440021F9F76FFA7C +:104FB00006208DF82000686A09960D909BE004F1A8 +:104FC0006C0304F15C020EA8FFF7C8FAE8E7397831 +:104FD000022903D139790029D0D0297592E28DF8C0 +:104FE0002000686A0D9056E538780728F6D1D4E994 +:104FF00009216078012808D004F16C00CDE9000295 +:10500000029105D104F16C0304E004F15C00F5E7C2 +:1050100004F15C0304F14C007A680646216AFFF74C +:1050200063F96078012822D1A078216AC0F11002CA +:105030000B1800211846F9F72AFAD4E90923606B06 +:1050400004F12D018DE80F0004F15C0300E05BE248 +:1050500004F16C0231460EA8FFF7C8F910220EA920 +:1050600004F13C00F9F7BCF908B10B20ACE485F879 +:10507000008000BF8DF82090686A0D908DF824A004 +:1050800009E538780528A9D18DF82000686A0D90C7 +:10509000B8680A90ADF824A08DF830B080F8008090 +:1050A000617801291AD0D4E9092104F12D03A66BF6 +:1050B00003910096CDE9013204F16C0304F15C0226 +:1050C00004F14C01401CFFF791F9002108A8FFF7FB +:1050D0008BFA6078012805D015203FE6D4E9091243 +:1050E000631DE4E70E20287006208DF82000686A12 +:1050F000CDF824B00D90A0788DF82800CBE4387856 +:105100000328C0D1E079C00770D00F202870072095 +:1051100065E7387804286BD11422391D04F1140096 +:10512000F9F78BF9616A208CA1F80900616AA0780F +:10513000C871E179626A01F003011172616A627AF1 +:105140000A73616AA07A81F8240016205DE485F86C +:1051500000A08DF82090696A50460D9192E0000001 +:10516000706202003878052842D1B868A861617879 +:10517000606A022901D0012100E0002101EB410118 +:10518000142606EBC1014058082102F0B0F96178FD +:10519000606A022901D0012100E0002101EB4101F8 +:1051A00006EBC101425802A8E169FFF70BFA6078EB +:1051B000626A022801D0012000E0002000EB4001DB +:1051C000102000EBC1000223105802A90932FEF79B +:1051D000EEFF626AFD4B0EA80932A169FFF7E1F903 +:1051E0006178606A022904D0012103E044E18DE086 +:1051F000BFE0002101EB4101182606EBC101A278B6 +:1052000040580EA9F9F719F96178606A022901D0AE +:10521000012100E0002101EB410106EBC1014158F1 +:10522000A0780B18C0F1100200211846F9F72FF9E9 +:1052300005208DF82000686A0D90A8690A90ADF8E5 +:1052400024A08DF830B0062101706278616A022ACC +:1052500001D0012200E0002202EB420206EBC20272 +:10526000401C89581022F9F7E8F8002108A8FFF738 +:10527000BBF91220C5F818B028708DF82090686A24 +:105280000D900B208DF8240005E43878052870D1A6 +:105290008DF82000686A0D90B8680A900B20ADF870 +:1052A00024000A98072101706178626A022901D0FE +:1052B000012100E0002101EB4103102101EBC301BA +:1052C00051580988A0F801106178626A022902D059 +:1052D000012101E02FE1002101EB4103142101EB49 +:1052E000C30151580A6840F8032F4968416059E0EA +:1052F0001920287001208DF8300074E616202870DF +:105300008DF830B0002108A8FFF76EF9032617E1E9 +:1053100014202870AEE6387805282AD18DF82000B0 +:10532000686A0D90B8680A90ADF824A08DF830B086 +:1053300080F800906278616A4E46022A01D001220C +:1053400000E0002202EB42021C2303EBC202401CDD +:1053500089581022F9F771F8002108A8FFF744F9DD +:10536000152028708DF82060686A0D908DF82460F3 +:1053700039E680E0387805287DD18DF82000686A0C +:105380000D90B8680A90ADF8249009210170616908 +:10539000097849084170616951F8012FC0F802206D +:1053A0008988C18020781C28A8D1A1E7E078C007AF +:1053B00002D04FF0060C01E04FF0070C6078022895 +:1053C0000AD000BF4FF0000000EB040101F1090119 +:1053D00005D04FF0010004E04FF00100F4E74FF07A +:1053E00000000B78204413EA0C030B7010F8092F0F +:1053F00002EA0C02027004D14FF01B0C84F800C0CA +:10540000D2B394F801C0BCF1010F00D09BB990F861 +:1054100000C0E0465FEACC7C04D028F001060670AC +:10542000102606E05FEA887C05D528F002060670A3 +:1054300013262E70032694F801C0BCF1020F00D091 +:1054400092B991F800C05FEACC7804D02CF0010644 +:105450000E70172106E05FEA8C7805D52CF0020665 +:105460000E701921217000260078D0BBCAB3C3BBCF +:105470001C20207035E012E002E03878062841D187 +:105480001A2015E4207801283CD00C283AD0204678 +:10549000FFF7EBF809208DF82000686A0D9031E0E5 +:1054A0003878052805D00620387003261820287083 +:1054B00046E005208DF82000696A0D91B9680A91CF +:1054C0000221ADF8241001218DF830100A990870DE +:1054D000287D4870394608A8FFF786F80646182048 +:1054E0002870012E0ED02BE001208DF82000686A74 +:1054F0000D9003208DF82400287D8DF8250085F877 +:1055000014B012E0287D80B11D2020701720287073 +:105510008DF82090686A0D9002208DF8240039469D +:1055200008A8FFF761F806460AE00CB1FE202070DB +:105530009DF8200020B1002108A8FFF755F80CE4E1 +:1055400013B03046BDE8F08F2DE9F04387B00C462C +:105550004E6900218DF804100120257803460227AA +:105560004FF007094FF0050C85B1012D53D0022DE6 +:1055700039D1FE2030708DF80030606A059003202C +:105580008DF80400207E8DF8050063E02179012963 +:1055900025D002292DD0032928D0042923D1B17D7B +:1055A000022920D131780D1F042D04D30A3D032D8B +:1055B00001D31D2917D12189022914D38DF8047034 +:1055C000237020899DF80410884201E0686202007F +:1055D00018D208208DF80000606A059057E07078B6 +:1055E0000128EBD0052007B0BDE8F0831D20307006 +:1055F000E4E771780229F5D131780C29F3D18DF8DF +:105600000490DDE7083402F804CB94E80B0082E84C +:105610000B000320E7E71578052DE4D18DF800C0D5 +:10562000656A0595956802958DF8101094F80480C8 +:10563000B8F1010F13D0B8F1020F2DD0B8F1030F5C +:105640001CD0B8F1040FCED1ADF804700E20287034 +:10565000207E687000216846FEF7C6FF0CE0ADF8BA +:1056600004700B202870207E002100F01F0068705D +:105670006846FEF7B9FF37700020B4E7ADF8047054 +:105680008DF8103085F800C0207E687027701146B4 +:105690006846FEF7A9FFA6E7ADF804902B70207FBF +:1056A0006870607F00F00100A870A07F00F01F000C +:1056B000E870E27F2A71C0071CD094F8200000F047 +:1056C0000700687194F8210000F00700A87100211C +:1056D0006846FEF789FF2868F062A8883086A879B6 +:1056E00086F83200A069407870752879B0700D2076 +:1056F0003070C1E7A9716971E9E700B587B0042886 +:105700000CD101208DF800008DF8040000200591D7 +:105710008DF8050001466846FEF766FF07B000BD3C +:1057200070B50C46054602F0C9F921462846BDE889 +:1057300070407823002202F017B908B10078704752 +:105740000C20704770B50C0005784FF000010CD0AC +:1057500021702146EFF7D1FD69482178405D8842EC +:1057600001D1032070BD022070BDEFF7C6FD0020FF +:1057700070BD0279012A05D000220A704B78012BF6 +:1057800002D003E0042070470A758A610279930011 +:10579000521C0271C15003207047F0B587B00F460C +:1057A00005460124287905EB800050F8046C7078D8 +:1057B000411E02290AD252493A46083901EB8000BB +:1057C000314650F8043C2846984704460CB1012C59 +:1057D00011D12879401E10F0FF00287101D0032458 +:1057E000E0E70A208DF80000706A0590002101961C +:1057F0006846FFF7A7FF032CD4D007B02046F0BDC2 +:1058000070B515460A46044629461046FFF7C5FFFF +:10581000064674B12078FE280BD1207C30B10020E0 +:105820002870294604F10C00FFF7B7FF2046FEF769 +:105830001CFF304670BD704770B50E4604467C2292 +:105840000021F8F724FE0225012E03D0022E04D0F9 +:10585000052070BD0120607000E065702046FEF7F5 +:1058600004FFA575002070BD28B1027C1AB10A465C +:1058700000F10C01C4E70120704710B5044686B062 +:10588000042002F01BF92078FE2806D000208DF8B5 +:10589000000069462046FFF7E7FF06B010BD7CB563 +:1058A0000E4600218DF804104178012903D0022909 +:1058B00003D0002405E0046900E044690CB1217CB8 +:1058C00089B16D4601462846FFF753FF032809D1E9 +:1058D000324629462046FFF793FF9DF80410002921 +:1058E00000D004207CBD04F10C05EBE730B40C467D +:1058F0000146034A204630BC024B0C3AFEF751BE2B +:10590000AC6202006862020070B50D46040011D05E +:1059100085B1220100212846F8F7B9FD102250492F +:105920002846F8F78AFD4F48012101704470456010 +:10593000002070BD012070BD70B505460024494EA1 +:1059400011E07068AA7B00EB0410817B914208D1C2 +:10595000C17BEA7B914204D10C222946F8F740FD35 +:1059600030B1641CE4B230788442EAD3002070BDC8 +:10597000641CE0B270BD70B50546FFF7DDFF00287E +:1059800005D1384C20786178884201D3002070BD61 +:105990006168102201EB00102946F8F74EFD2078CF +:1059A000401CC0B2207070BD2E48007870472D4951 +:1059B0000878012802D0401E08700020704770B59A +:1059C0000D460021917014461180022802D0102843 +:1059D00015D105E0288890B10121A17010800CE05C +:1059E000284613B1FFF7C7FF01E0FFF7A5FFA0703E +:1059F00010F0FF0F03D0A8892080002070BD012087 +:105A000070BD0023DBE770B5054614460E0009D0D3 +:105A100000203070A878012806D003D911490A78EF +:105A200090420AD9012070BD24B1287820702888BE +:105A3000000A5070022008700FE064B1496810221B +:105A400001EB001120461039F8F7F7FC2878207395 +:105A50002888000A607310203070002070BD00009C +:105A6000BB620200900000202DE9F04190460C46F8 +:105A700007460025FE48072F00EB881607D2DFE80F +:105A800007F00707070704040400012500E0FFDF13 +:105A900006F81470002D13D0F548803000EB880113 +:105AA00091F82700202803D006EB4000447001E065 +:105AB00081F8264006EB44022020507081F82740F0 +:105AC000BDE8F081F0B51F4614460E460546202A73 +:105AD00000D1FFDFE649E648803100EB871C0CEB84 +:105AE000440001EB8702202E07D00CEB46014078E2 +:105AF0004B784870184620210AE092F8253040780B +:105B000082F82500F6E701460CEB4100057040786D +:105B1000A142F8D192F82740202C03D00CEB44048A +:105B2000637001E082F826300CEB4104202363709F +:105B300082F82710F0BD30B50D46CE4B4419002237 +:105B4000181A72EB020100D2FFDFCB48854200DD5C +:105B5000FFDFC9484042854200DAFFDFC548401CEC +:105B6000844207DA002C01DB204630BDC148401CCE +:105B7000201830BDBF48C043FAE710B5044601689D +:105B8000407ABE4A52F82020114450B10220084405 +:105B900020F07F40EDF763F894F90810BDE810405D +:105BA000C9E70420F3E72DE9F047B14E803696F8B7 +:105BB0002D50DFF8BC9206EB850090F8264034E0CB +:105BC00009EB85174FF0070817F81400012806D0D5 +:105BD00004282ED005282ED0062800D0FFDF01F0A3 +:105BE00025F9014607EB4400427806EB850080F872 +:105BF000262090F82720A24202D1202280F82720D8 +:105C0000084601F01EF92A4621460120FFF72CFF25 +:105C10009B48414600EB041002682046904796F8E6 +:105C20002D5006EB850090F82640202CC8D1BDE809 +:105C3000F087022000E003208046D0E710B58C4CAE +:105C40002021803484F8251084F8261084F8271049 +:105C5000002084F8280084F82D0084F82E10411EBE +:105C6000A16044F8100B2074607420736073A073FB +:105C70008449E07720750870487000217C4A103C08 +:105C800002F81100491CC9B22029F9D30120ECF710 +:105C9000D6FE0020ECF7D3FE012084F82200EDF7B9 +:105CA000FFF87948EDF711F9764CA41E207077487B +:105CB000EDF70BF96070BDE81040ECF74DBE10B584 +:105CC000ECF76FFE6F4CA41E2078EDF717F96078A3 +:105CD000EDF714F9BDE8104001F0E0B8202070475E +:105CE0000020ECF785BE70B5054601240E46AC4099 +:105CF0005AB1FFF7F5FF0146654800EBC500C0F853 +:105D00001015C0F81465634801E06248001D046086 +:105D100070BD2DE9F34F564C0025803404EB810A09 +:105D200089B09AF82500202821D0691E0291544993 +:105D3000009501EB0017391D03AB07C983E8070085 +:105D4000A18BADF81C10A07F8DF81E009DF81500EA +:105D5000A046C8B10226494951F820400399A2192A +:105D6000114421F07F41019184B102210FE0012013 +:105D7000ECF765FE0020ECF762FEECF730FE01F078 +:105D80008DF884F82F50A9E00426E4E700218DF86F +:105D90001810022801D0012820D103980119099870 +:105DA000081A801C9DF81C1020F07F4001B10221D0 +:105DB000353181420BD203208DF815000398C4F1D0 +:105DC0003201401A20F07F40322403900CE098F812 +:105DD000240018B901F043FA002863D0322C03D212 +:105DE00014B101F04FF801E001F058F8254A10789D +:105DF00018B393465278039B121B00219DF818405C +:105E0000994601281AD0032818D000208DF81E00CA +:105E1000002A04DD981A039001208DF818009DF8DF +:105E20001C0000B1022103981B4A20F07F40039020 +:105E300003AB099801F03EF810B110E00120E5E74E +:105E40009DF81D0018B99BF80000032829D08DF893 +:105E50001C50CDF80C908DF818408DF81E509DF810 +:105E6000180010B30398012381190022184615E089 +:105E7000840A0020FF7F841E0020A107CC6202005C +:105E8000840800209A00002017780100A75B010019 +:105E900000F0014004F50140FFFF3F00ECF722FE57 +:105EA00006E000200BB0BDE8F08F0120ECF7C7FD45 +:105EB00097F90C20012300200199ECF713FEF87BE1 +:105EC000C00701D0ECF7F7FE012188F82F108AF8FF +:105ED000285020226946FE48F8F7AFFA0120E1E792 +:105EE0002DE9F05FDFF8E883064608EB860090F8BE +:105EF0002550202D1FD0A8F180002C4600EB8617DE +:105F0000A0F50079DFF8CCB305E0A24607EB4A0024 +:105F10004478202C0AD0ECF730FE09EB04135A46E3 +:105F200001211B1D00F0C6FF0028EED0AC4202D0BC +:105F3000334652461EE0E84808B1AFF30080ECF764 +:105F40001CFE98F82F206AB1D8F80C20411C891A41 +:105F50000902CA1701EB12610912002902DD0020B3 +:105F6000BDE8F09F3146FFF7D4FE08B10120F7E706 +:105F700033462A4620210420FFF7A4FDEFE72DE950 +:105F8000F041D34C2569ECF7F8FD401B0002C11726 +:105F900000EB1160001200D4FFDF94F8220000B182 +:105FA000FFDF012784F8227094F82E00202800D10A +:105FB000FFDF94F82E60202084F82E00002584F85E +:105FC0002F5084F8205084F82150C4482560007870 +:105FD000022833D0032831D000202077A068401C4D +:105FE00005D04FF0FF30A0600120ECF728FD002025 +:105FF000ECF725FDECF721FEECF719FEECF7EFFCD2 +:106000000EF0D6FDB648056005604FF0E0214FF474 +:106010000040B846C1F88002ECF7BBFE94F82D7042 +:106020003846FFF75DFF0028FAD0A948803800EB1A +:10603000871010F81600022802D006E00120CCE7F5 +:106040003A4631460620FFF70FFD84F8238004EB23 +:10605000870090F82600202804D0A048801E4078B1 +:10606000ECF752FF207F002803D0ECF7D6FD257710 +:10607000657725E5964910B591F82D2000248039E3 +:1060800001EB821111F814302BB1641CE4B2202C06 +:10609000F8D3202010BD934901EB041108600020C3 +:1060A000C87321460120FFF7DFFC204610BD10B564 +:1060B000012801D0032800D171B3854A92F82D3010 +:1060C000834C0022803C04EB831300BF13F8124082 +:1060D0000CB1082010BD521CD2B2202AF6D37F4A40 +:1060E00048B1022807D0072916D2DFE801F01506CB +:1060F000080A0C0E100000210AE01B2108E03A21DA +:1061000006E0582104E0772102E0962100E0B52165 +:1061100051701070002010BD072010BD6F4810B5E1 +:106120004078ECF79CFD80B210BD10B5202811D24C +:10613000674991F82D30A1F1800202EB831414F825 +:1061400010303BB191F82D3002EB831212F8102081 +:10615000012A01D0002010BD91F82D200146002019 +:10616000FFF782FC012010BD10B5ECF706FDBDE87D +:106170001040ECF774BD2DE9F0410E46544F017804 +:106180002025803F0C4607EB831303E0254603EBF5 +:1061900045046478944202D0202CF7D108E0202CEA +:1061A00006D0A14206D103EB41014978017007E016 +:1061B000002085E403EB440003EB45014078487080 +:1061C000494F7EB127B1002140F22D40AFF300804E +:1061D0003078A04206D127B100214FF48660AFF39A +:1061E0000080357027B1002140F23540AFF30080C8 +:1061F000012065E410B542680B689A1A1202D417A0 +:1062000002EB1462121216D4497A91B1427A82B921 +:10621000364A006852F82110126819441044001DD3 +:10622000891C081A0002C11700EB11600012322805 +:1062300001DB012010BD002010BD2DE9F047294EE3 +:10624000814606F500709846144600EB811712E06F +:1062500006EB0415291D4846FFF7CCFF68B988F8FE +:106260000040A97B99F80A00814201D80020DEE4B1 +:1062700007EB44004478202CEAD10120D7E42DE933 +:10628000F047824612480E4600EB8600DFF8548045 +:1062900090F825402020107008F5007099461546AA +:1062A00000EB86170BE000BF08EB04105146001D01 +:1062B000FFF7A0FF28B107EB44002C704478202C96 +:1062C000F2D1297889F800104B46224631460FE07A +:1062D000040B0020FFFF3F00000000009A00002098 +:1062E00000F500408408002000000000CC6202009D +:1062F0005046BDE8F047A0E72DE9FC410F460446B3 +:106300000025FE4E10E000BF9DF80000256806EB5A +:1063100000108168204600F0E1FD2068A84202D10B +:106320000020BDE8FC8101256B4601AA39462046C4 +:10633000FFF7A5FF0028E7D02846F2E770B504462E +:10634000EF480125A54300EB841100EB85104022A6 +:10635000F8F773F8EB4E26B1002140F29D40AFF301 +:106360000080E748803000EB850100EB8400D0F826 +:106370002500C1F8250026B1002140F2A140AFF36D +:106380000080284670BD8A4203D003460520FFF7EF +:1063900099BB202906D0DA4A02EB801000EB4100BD +:1063A00040787047D649803101EB800090F8250095 +:1063B0007047D24901EB0010001DFFF7DEBB7CB532 +:1063C0001D46134604460E4600F1080221461846B3 +:1063D000ECF752FC94F908000F2804DD1F382072F6 +:1063E0002068401C206096B10220C74951F8261051 +:1063F000461820686946801B20F07F40206094F991 +:1064000008002844C01C1F2803DA012009E00420EA +:10641000EBE701AAECF730FC9DF8040010B10098FE +:10642000401C00900099206831440844C01C20F0B2 +:106430007F4060607CBDFEB50C46064609786079F9 +:10644000907220791F461546507279B12179002249 +:106450002846A368FFF7B3FFA9492846803191F881 +:106460002E20202A0AD00969491D0DE0D4E9022313 +:10647000217903B02846BDE8F040A0E7A349497858 +:10648000052900D20521314421F07F4100F026FD8D +:1064900039462846FFF730FFD4E9023221796846B1 +:1064A000FFF78DFF2B4600213046019A00F002FDD8 +:1064B000002806D103B031462846BDE8F04000F080 +:1064C0000DBDFEBD2DE9F14F84B000F0C3FCF0B16D +:1064D00000270498007800284FF000006DD1884D07 +:1064E000884C82468346803524B1002140F2045016 +:1064F000AFF3008095F82D8085F823B0002624B1F5 +:10650000002140F20950AFF3008017B105E00127E8 +:10651000DFE74046FFF712FF804624B1002140F23A +:106520001150AFF30080ECF728FB814643466A46E2 +:106530000499FFF780FF24B1002140F21750AFF318 +:10654000008095F82E0020280CD029690098401A68 +:106550000002C21700EB1260001203D5684600F07B +:10656000BDFC01264CB1002140F22150AFF3008068 +:10657000002140F22650AFF300806B46644A0021B0 +:10658000484600F097FC98B127B941466846FFF7A6 +:10659000B3FE064326B16846FFF7EFFA0499886018 +:1065A0004FF0010A24B1002140F23A50AFF30080CD +:1065B00095F82300002897D1504605B073E42DE9E3 +:1065C000F04F89B08B46824600F044FC4C4C80343E +:1065D00030B39BF80000002710B1012800D0FFDF86 +:1065E000484D25B1002140F2F950AFF300804349F6 +:1065F000012001EB0A18A946CDF81C005FEA090644 +:1066000004D0002140F20160AFF30080079800F051 +:1066100018FC94F82D50002084F8230067B119E08D +:1066200094F82E000127202800D1FFDF9BF80000FE +:106630000028D5D0FFDFD3E72846FFF77FFE0546C9 +:1066400026B1002140F20B60AFF3008094F82300E4 +:106650000028D3D126B1002140F21560AFF30080AD +:10666000ECF78BFA2B4602AA59460790FFF7E3FE98 +:1066700098F80F005FEA060900F001008DF813009A +:1066800004D0002140F21F60AFF300803B462A4651 +:1066900002A9CDF800A0079800F02BFC064604EBF9 +:1066A000850090F828000090B9F1000F04D0002177 +:1066B00040F22660AFF3008000F0B8FB0790B9F11C +:1066C000000F04D0002140F22C60AFF3008094F85A +:1066D0002300002892D1B9F1000F04D0002140F22C +:1066E0003460AFF300800DF1080C9CE80E00C8E99F +:1066F0000112C8F80C30BEB30CE000008408002082 +:10670000840A002000000000CC6202009A000020F1 +:10671000FFFF3F005FEA090604D0002140F241601C +:10672000AFF300800098B84312D094F82E002028D0 +:106730000ED126B1002140F24660AFF3008028461A +:10674000FFF7CEFB20B99BF80000D8B3012849D051 +:10675000B9F1000F04D0002140F26360AFF3008074 +:10676000284600F05CFB01265FEA090504D0002101 +:1067700040F26C60AFF30080079800F062FB25B137 +:1067800000214FF4CE60AFF300808EB194F82D005D +:1067900004EB800090F82600202809D025B10021C4 +:1067A00040F27760AFF30080F7484078ECF7ACFB3D +:1067B00025B1002140F27C60AFF3008009B0304683 +:1067C000BDE8F08FFFE7B9F1000F04D0002140F2DF +:1067D0004E60AFF3008094F82D2051460420FFF75F +:1067E00043F9C0E7002E3FF409AF002140F25960A1 +:1067F000AFF3008002E72DE9F84FE44D814695F8AC +:106800002D004FF00008E24C4FF0010B474624B139 +:10681000002140F28A60AFF30080584600F011FB7F +:1068200085F8237024B1002140F28F60AFF300801F +:1068300095F82D00FFF782FD064695F8230028B154 +:10684000002CE4D0002140F295604BE024B10021FF +:1068500040F29960AFF30080CC48803800EB86119D +:1068600011F81900032856D1334605EB830A4A462E +:106870009AF82500904201D1012000E0002000900C +:106880000AF125000021FFF776FC0146009801423D +:1068900003D001228AF82820AF77E1B324B1002188 +:1068A00040F29E60AFF30080324649460120FFF778 +:1068B000DBF89AF828A024B1002140F2A960AFF3D8 +:1068C000008000F0B3FA834624B1002140F2AE60AC +:1068D000AFF3008095F8230038B1002C97D0002149 +:1068E00040F2B260AFF3008091E7BAF1000F07D039 +:1068F00095F82E00202803D13046FFF7F1FAE0B1D9 +:1069000024B1002140F2C660AFF30080304600F0B1 +:1069100086FA4FF0010824B1002140F2CF60AFF3B6 +:106920000080584600F08DFA24B1002140F2D36077 +:10693000AFF300804046BDE8F88F002CF1D0002175 +:1069400040F2C160AFF30080E6E70120ECF750B8F9 +:106950008D48007870472DE9F0418C4C94F82E005A +:1069600020281FD194F82D6004EB860797F8255056 +:10697000202D00D1FFDF8549803901EB861000EB27 +:106980004500407807F8250F0120F87084F82300AF +:10699000294684F82E50324602202234FFF764F84C +:1069A0000020207005E42DE9F0417A4E774C012556 +:1069B00038B1012821D0022879D003287DD0FFDF0B +:1069C00017E400F05FFAFFF7C6FF207E00B1FFDF9B +:1069D00084F821500020ECF732F8A168481C04D05C +:1069E000012300221846ECF77DF814F82E0F2178C9 +:1069F00006EB01110A68012154E0FFF7ACFF01200A +:106A0000ECF71DF894F8210050B1A068401C07D0A5 +:106A100014F82E0F217806EB01110A68062141E0D7 +:106A2000207EDFF86481002708F10208012803D0E6 +:106A300002281ED0FFDFB5E7A777ECF7EEF898F84D +:106A40000000032801D165772577607D524951F810 +:106A5000200094F8201051B948B161680123091A47 +:106A600000221846ECF73EF8022020769AE72776B7 +:106A700098E784F8205000F005FAA07F50B198F80C +:106A8000010061680123091A00221846ECF72AF870 +:106A9000257600E0277614F82E0F217806EB0111F9 +:106AA0000A680021BDE8F041104700E005E03648E3 +:106AB0000078BDE8F041ECF727BAFFF74CFF14F877 +:106AC0002E0F217806EB01110A680521EAE710B5BF +:106AD0002E4C94F82E00202800D1FFDF14F82E0F42 +:106AE00021782C4A02EB01110A68BDE8104004210C +:106AF00010477CB5254C054694F82E00202800D17F +:106B0000FFDFA068401C00D0FFDF94F82E00214971 +:106B100001AA01EB0010694690F90C002844ECF73B +:106B2000ABF89DF904000F2801DD012000E00020F2 +:106B3000009908446168084420F07F41A16094F8FE +:106B40002100002807D002B00123BDE870400022D8 +:106B50001846EBF7C7BF7CBD30B5104A0B1A541C62 +:106B6000B3EB940F1ED3451AB5EB940F1AD393428F +:106B700003D9101A43185B1C14E0954210D9511A1E +:106B80000844401C43420DE098000020040B002004 +:106B90000000000084080020CC620200FF7F841EF9 +:106BA000FFDF0023184630BD0123002201460220EA +:106BB000EBF798BF0220EBF742BFEBF7DEBF2DE902 +:106BC000FE4FEE4C05468A4694F82E00202800D150 +:106BD000FFDFEA4E94F82E10A0462046A6F520725C +:106BE00002EB011420218DF8001090F82D10376968 +:106BF00000EB8101D8F8000091F82590284402AA02 +:106C000001A90C36ECF738F89DF90800002802DDE0 +:106C10000198401C0190A0680199642D084452D34A +:106C2000D74B00225B1B72EB02014CD36168411A07 +:106C300021F07F41B1F5800F45D220F07F40706098 +:106C400086F80AA098F82D1044466B464A4630460E +:106C5000FFF7F3FAB0B3A068401C10D0EBF78DFF3C +:106C6000A168081A0002C11700EB11600012022887 +:106C70002BDD0120EBF7E3FE4FF0FF30A06094F82E +:106C80002D009DF8002020210F34FFF77CFBA17F11 +:106C9000BA4A803A02EB8111E27F01EB420148706F +:106CA00054F80F0C284444F80F0C012020759DF86F +:106CB0000000202803D0B3484078ECF725F90120E4 +:106CC000BDE8FE8F01E00020FAE77760FBE72DE9E1 +:106CD000F047AA4C074694F82D00A4F1800606EB75 +:106CE000801010F8170000B9FFDF94F82D50A0466F +:106CF000A54C24B1002140F6EA00AFF3008040F635 +:106D0000F60940F6FF0A06EB851600BF16F81700D5 +:106D1000012819D0042811D005280FD006280DD03D +:106D20001CB100214846AFF300800FF02DF8002C75 +:106D3000ECD000215046AFF30080E7E72A46394601 +:106D40000120FEF791FEF2E74FF0010A4FF0000933 +:106D5000454624B1002140F60610AFF300805046AE +:106D600000F06FF885F8239024B1002140F60B1055 +:106D7000AFF3008095F82D00FFF7E0FA064695F88E +:106D8000230028B1002CE4D0002140F611101FE0B0 +:106D900024B1002140F61510AFF3008005EB86000A +:106DA00000F1270133463A462630FFF7E4F924B1D3 +:106DB000002140F61910AFF3008000F037F882464A +:106DC00095F8230038B1002CC3D0002140F61F10E5 +:106DD000AFF30080BDE785F82D60012085F8230022 +:106DE000504600F02EF8002C04D0002140F62C1064 +:106DF000AFF30080BDE8F08730B504465F480D462C +:106E000090F82D005D49803901EB801010F81400D6 +:106E100000B9FFDF5D4800EB0410C57330BD574972 +:106E200081F82D00012081F82300704710B55848E3 +:106E300008B1AFF30080EFF3108000F0010072B6EC +:106E400010BD10B5002804D1524808B1AFF300803E +:106E500062B610BD50480068C005C00D10D0103893 +:106E600040B2002804DB00F1E02090F8000405E0C7 +:106E700000F00F0000F1E02090F8140D4009704779 +:106E80000820704710B53D4C94F82400002804D128 +:106E9000F4F712FF012084F8240010BD10B5374C20 +:106EA00094F82400002804D0F4F72FFF002084F881 +:106EB000240010BD10B51C685B68241A181A24F051 +:106EC0007F4420F07F40A14206D8B4F5800F03D262 +:106ED000904201D8012010BD002010BDD0E9003241 +:106EE000D21A21F07F43114421F07F41C0E90031E3 +:106EF00070472DE9FC418446204815468038089C9F +:106F000000EB85160F4616F81400012804D002285D +:106F100002D00020BDE8FC810B46204A01216046DA +:106F2000FFF7C8FFF0B101AB6A4629463846FFF7C4 +:106F3000A6F9B8B19DF804209DF800102846FFF787 +:106F400022FA06EB440148709DF8000020280DD07D +:106F500006EB400044702A4621460320FEF784FDDC +:106F60000120D7E72A4621460420F7E703480121FC +:106F700000EB850000F8254FC170ECE7040B002002 +:106F8000FF1FA107980000200000000084080020D7 +:106F9000000000000000000004ED00E0FFFF3F00E3 +:106FA0002DE9F041044680074FF000054FF001063F +:106FB0000CD56B480560066000F0E8F920B169481F +:106FC000016841F48061016024F00204E0044FF0A4 +:106FD000FF3705D564484660C0F8087324F4805430 +:106FE000600003D56148056024F08044E0050FD5BA +:106FF0005F48C0F80052C0F808735E490D60091D73 +:107000000D605C4A04210C321160066124F4807426 +:10701000A00409D558484660C0F80052C0F808736B +:107020005648056024F40054C4F38030C4F3C031E2 +:10703000884200D0FFDF14F4404F14D0504846601F +:10704000C0F808734F488660C0F80052C0F8087353 +:107050004D490D600A1D16608660C0F808730D600A +:10706000166024F4404420050AD5484846608660EE +:10707000C0F80873C0F848734548056024F40064FC +:107080000DF070FD4348044200D0FFDFBDE8F08101 +:10709000F0B50022202501234FEA020420FA02F174 +:1070A000C9072DD051B2002910DB00BF4FEA51179C +:1070B0004FEA870701F01F0607F1E02703FA06F6FB +:1070C000C7F88061BFF34F8FBFF36F8F0CDB00BF3A +:1070D0004FEA51174FEA870701F01F0607F1E02733 +:1070E00003FA06F6C7F8806204DB01F1E02181F8BB +:1070F000004405E001F00F0101F1E02181F8144D99 +:1071000002F10102AA42C9D3F0BD10B5224C2060A1 +:107110000846F4F7EAFE2068FFF742FF2068FFF711 +:10712000B7FF0DF045F900F092F90DF01BFD0DF0E1 +:1071300058FCEBF7B5FEBDE810400DF0EDB910B509 +:10714000154C2068FFF72CFF2068FFF7A1FF0DF01A +:1071500009FDF4F7C9FF0020206010BD0A20704728 +:10716000FC1F00403C17004000C0004004E5014007 +:10717000008000400485004000D0004004D500405D +:1071800000E0004000F0004000F5004000B000408A +:1071900008B50040FEFF0FFD9C00002070B5264999 +:1071A0000A680AB30022154601244B685B1C4B6039 +:1071B0000C2B00D34D600E7904FA06F30E681E42C4 +:1071C0000FD0EFF3108212F0010272B600D001224C +:1071D0000C689C430C6002B962B6496801600020EB +:1071E00070BD521C0C2AE0D3052070BD4FF0E02189 +:1071F0004FF48000C1F800027047EFF3108111F0E6 +:10720000010F72B64FF0010202FA00F20A48036859 +:1072100042EA0302026000D162B6E7E706480021B5 +:1072200001604160704701218140034800680840C7 +:1072300000D0012070470000A0000020012081073D +:10724000086070470121880741600021C0F80011E3 +:1072500018480170704717490120087070474FF0B7 +:107260008040D0F80001012803D01248007800289F +:1072700000D00120704710480068C00700D00120EE +:1072800070470D480C300068C00700D001207047DF +:107290000948143000687047074910310A68D20362 +:1072A00006D5096801F00301814201D10120704730 +:1072B00000207047A8000020080400404FF08050D4 +:1072C000D0F830010A2801D0002070470120704713 +:1072D00000B5FFF7F3FF20B14FF08050D0F8340134 +:1072E00008B1002000BD012000BD4FF08050D0F853 +:1072F00030010E2801D000207047012070474FF068 +:107300008050D0F83001062803D0401C01D0002066 +:107310007047012070474FF08050D0F830010D28A1 +:1073200001D000207047012070474FF08050D0F806 +:107330003001082801D000207047012070474FF02D +:107340008050D0F83001102801D000207047012073 +:10735000704700B5FFF7F3FF30B9FFF7DCFF18B94E +:10736000FFF7E3FF002800D0012000BD00B5FFF7C4 +:10737000C6FF38B14FF08050D0F83401062803D34F +:10738000401C01D0002000BD012000BD00B5FFF76A +:10739000B6FF48B14FF08050D0F83401062803D32F +:1073A000401C01D0012000BD002000BD0021017063 +:1073B000084670470146002008707047EFF31081BF +:1073C00001F0010172B60278012A01D0012200E029 +:1073D00000220123037001B962B60AB10020704790 +:1073E0004FF400507047E9E7EFF3108111F0010FFF +:1073F00072B64FF00002027000D162B600207047F2 +:10740000F2E700002DE9F04115460E46044600273C +:1074100000F0EBF8A84215D3002341200FE000BF95 +:1074200094F84220A25CF25494F84210491CB1FB3B +:10743000F0F200FB12115B1C84F84210DBB2AB428D +:10744000EED3012700F0DDF83846BDE8F08172493F +:1074500010B5802081F800047049002081F84200B6 +:1074600081F84100433181F8420081F84100433105 +:1074700081F8420081F841006948FFF797FF6848AA +:10748000401CFFF793FFEBF793FCBDE8104000F0C2 +:10749000B8B840207047614800F0A7B80A460146D6 +:1074A0005E48AFE7402070475C48433000F09DB82D +:1074B0000A46014659484330A4E7402101700020A4 +:1074C000704710B504465548863000F08EF820709D +:1074D000002010BD0A460146504810B58630FFF71F +:1074E00091FF08B1002010BD42F2070010BD70B539 +:1074F0000C460646412900D9FFDF4A48006810388B +:1075000040B200F054F8C5B20D2000F050F8C0B2FF +:10751000854201D3012504E0002502E00DB1EBF71F +:107520008AFC224631463D48FFF76CFF0028F5D023 +:1075300070BD2DE9F0413A4F0025064617F10407CA +:1075400057F82540204600F041F810B36D1CEDB20D +:10755000032DF5D33148433000F038F8002825D00A +:107560002E4800F033F8002820D02C48863000F058 +:107570002DF800281AD0EBF734FC2948FFF71EFF3E +:10758000B0F5005F00D0FFDFBDE8F0412448FFF711 +:107590002BBF94F841004121265414F8410F401CA0 +:1075A000B0FBF1F201FB12002070D3E74DE7002899 +:1075B00004DB00F1E02090F8000405E000F00F008B +:1075C00000F1E02090F8140D4009704710F8411FB9 +:1075D0004122491CB1FBF2F302FB131140788142B6 +:1075E00001D1012070470020704710F8411F4078FA +:1075F000814201D3081A02E0C0F141000844C0B240 +:10760000704710B50648FFF7D9FE002803D1BDE842 +:107610001040EBF7D1BB10BD0DE000E0340B0020B3 +:10762000AC00002004ED00E070B5154D2878401C3A +:10763000C4B26878844202D000F0DBFA2C7070BDCE +:107640002DE9F0410E4C4FF0E02600BF00F0C6FAE5 +:107650000EF09AFB40BF20BF677820786070D6F8A4 +:107660000052E9F798FE854305D1D6F8040210B917 +:107670002078B842EAD000F0ACFA0020BDE8F081F2 +:10768000BC0000202DE9F04101264FF0E02231033B +:107690004FF000084046C2F88011BFF34F8FBFF390 +:1076A0006F8F204CC4F800010C2000F02EF81E4D06 +:1076B0002868C04340F30017286840F01000286095 +:1076C000C4F8046326607F1C02E000BF0EF05CFB80 +:1076D000D4F800010028F9D01FB9286820F0100064 +:1076E0002860124805686660C4F80863C4F8008121 +:1076F0000C2000F00AF82846BDE8F08110B50446D9 +:10770000FFF7C0FF2060002010BD002809DB00F05B +:107710001F02012191404009800000F1E020C0F8E3 +:107720008012704700C0004010ED00E008C5004026 +:107730002DE9F047FF4C0646FF21A06800EB06123A +:1077400011702178FF2910D04FF0080909EB0111C1 +:1077500009EB06174158C05900F0F4F9002807DD7D +:10776000A168207801EB061108702670BDE8F0874B +:1077700094F8008045460DE0A06809EB05114158DA +:10778000C05900F0DFF9002806DCA068A84600EB2D +:1077900008100578FF2DEFD1A06800EB061100EB73 +:1077A00008100D700670E1E7F0B5E24B04460020CA +:1077B00001259A680C269B780CE000BF05EB0017AA +:1077C000D75DA74204D106EB0017D7598F4204D0EA +:1077D000401CC0B28342F1D8FF20F0BD70B5FFF766 +:1077E000ECF9D44C08252278A16805EB02128958DF +:1077F00000F0A8F9012808DD2178A06805EB011147 +:107800004058BDE87040FFF7CFB9FFF7A1F8BDE8D9 +:107810007040EBF779BB2DE9F041C64C2578FFF7B6 +:10782000CCF9FF2D6ED04FF00808A26808EB0516C2 +:10783000915900F087F90228A06801DD80595DE0C8 +:1078400000EB051109782170022101EB0511425C62 +:107850005AB1521E4254815901F5800121F07F41F5 +:1078600081512846FFF764FF34E00423012203EB33 +:10787000051302EB051250F803C0875CBCF1000F42 +:1078800010D0BCF5007F10D9CCF3080250F806C028 +:107890000CEB423C2CF07F4C40F806C0C3589A1ABF +:1078A000520A09E0FF2181540AE0825902EB4C326E +:1078B00022F07F428251002242542846FFF738FFCF +:1078C0000C21A06801EB05114158E06850F8272011 +:1078D000384690472078FF2814D0FFF76EF92278B9 +:1078E000A16808EB02124546895800F02BF90128DF +:1078F00093DD2178A06805EB01114058BDE8F04107 +:10790000FFF752B9BDE8F081F0B51D4614460E46AA +:107910000746FF2B00D3FFDFA00700D0FFDF85481D +:10792000FF210022C0E90247C57006710170427054 +:1079300082701046012204E002EB0013401CE15467 +:10794000C0B2A842F8D3F0BD70B57A4C064665784F +:107950002079854200D3FFDFE06840F82560607839 +:10796000401C6070284670BD2DE9FF5F1D468B46A8 +:107970000746FF24FFF721F9DFF8B891064699F88A +:107980000100B84200D8FFDF00214FF001084FF09E +:107990000C0A99F80220D9F808000EE008EB011350 +:1079A000C35CFF2B0ED0BB4205D10AEB011350F88C +:1079B00003C0DC450CD0491CC9B28A42EED8FF2C6A +:1079C00002D00DE00C46F6E799F803108A4203D185 +:1079D000FF2004B0BDE8F09F1446521C89F8022035 +:1079E00008EB04110AEB0412475440F802B00421DA +:1079F000029B0022012B01EB04110CD040F8012066 +:107A00004FF4007808234FF0020C454513D9E905DF +:107A1000C90D02D002E04550F2E7414606EB413283 +:107A200003EB041322F07F42C250691A0CEB0412DC +:107A3000490A81540BE005B9012506EB453103EBFA +:107A4000041321F07F41C1500CEB0411425499F80A +:107A500000502046FFF76CFE99F80000A84201D0C4 +:107A6000FFF7BCFE3846B4E770B50C460546FFF795 +:107A7000A4F8064621462846FFF796FE0446FF284E +:107A80001AD02C4D082101EB0411A868415830464A +:107A900000F058F800F58050C11700EBD1404013BA +:107AA0000221AA6801EB0411515C09B100EB4120ED +:107AB000002800DC012070BD002070BD2DE9F047DA +:107AC00088468146FFF770FE0746FF281BD0194DF8 +:107AD0002E78A8683146344605E0BC4206D02646DA +:107AE00000EB06121478FF2CF7D10CE0FF2C0AD023 +:107AF000A6420CD100EB011000782870FF2804D0BA +:107B0000FFF76CFE03E0002030E6FFF753F8414634 +:107B10004846FFF7A9FF0123A968024603EB0413B7 +:107B2000FF20C854A878401EB84200D1A87001EBCD +:107B3000041001E0000C002001EB06110078087031 +:107B4000104613E6081A0002C11700EB116000127C +:107B50007047000010B5202000F07FF8202000F0D2 +:107B60008DF84D49202081F80004E9F712FC4B49BB +:107B700008604B48D0F8041341F00101C0F8041329 +:107B8000D0F8041341F08071C0F804134249012079 +:107B90001C39C1F8000110BD10B5202000F05DF8BF +:107BA0003E480021C8380160001D01603D4A481E62 +:107BB00010603B4AC2F80803384B1960C2F8000154 +:107BC000C2F8600138490860BDE81040202000F08C +:107BD00055B834493548091F086070473149334862 +:107BE000086070472D48C8380160001D521E0260B1 +:107BF00070472C4901200860BFF34F8F70472DE973 +:107C0000F0412849D0F8188028480860244CD4F85E +:107C100000010025244E6F1E28B14046E9F712FBF3 +:107C200040B9002111E0D4F8600198B14046E9F76D +:107C300009FB48B1C4F80051C4F860513760BDE891 +:107C4000F041202000F01AB831684046BDE8F0410C +:107C50000EF0A4B8FFDFBDE8F08100280DDB00F0D6 +:107C60001F02012191404009800000F1E020C0F88E +:107C70008011BFF34F8FBFF36F8F7047002809DB70 +:107C800000F01F02012191404009800000F1E02036 +:107C9000C0F880127047000020E000E0C8060240F3 +:107CA00000000240180502400004024001000001EB +:107CB0005E4800210170417010218170704770B5DD +:107CC000054616460C460220EAF714FE57490120E5 +:107CD00008705749F01E086056480560001F046090 +:107CE00070BD10B50220EAF705FE5049012008706A +:107CF00051480021C0F80011C0F80411C0F8081163 +:107D00004E494FF40000086010BD48480178D9B1D1 +:107D10004B4A4FF4000111604749D1F8003100226D +:107D2000002B1CBFD1F80431002B02D0D1F8081170 +:107D300019B142704FF0100104E04FF001014170A1 +:107D400040490968817002704FF00000EAF7D2BD27 +:107D500010B50220EAF7CEFD34480122002102705E +:107D60003548C0F80011C0F80411C0F808110260CD +:107D700010BD2E480178002904BF407870472E4876 +:107D8000D0F80011002904BF02207047D0F800117C +:107D900000291CBFD0F80411002905D0D0F8080133 +:107DA000002804BF01207047002070471F4800B51D +:107DB0000278214B4078C821491EC9B282B1D3F85C +:107DC00000C1BCF1000F10D0D3F8000100281CBF87 +:107DD000D3F8040100280BD0D3F8080150B107E014 +:107DE000022802D0012805D002E00029E4D1FFDFFB +:107DF000002000BD012000BD0C480178002904BF0F +:107E0000807870470C48D0F8001100291CBFD0F8CA +:107E10000411002902D0D0F8080110B14FF0100071 +:107E2000704708480068C0B270470000BE000020DC +:107E300010F5004008F5004000F0004004F5014056 +:107E400008F5014000F400405748002101704170DE +:107E5000704770B5064614460D460120EAF74AFD04 +:107E600052480660001D0460001D05605049002056 +:107E7000C1F850014F490320086050494E4808603E +:107E8000091D4F48086070BD2DE9F0410546464880 +:107E90000C46012606704B4945EA024040F08070CE +:107EA0000860FFF72CFA002804BF47480460002749 +:107EB000464CC4F80471474945480860002D02BF8C +:107EC000C4F800622660BDE8F081012D18BFFFDF15 +:107ED000C4F80072266041493F480860BDE8F0815F +:107EE0003148017871B13B4A394911603749D1F8BD +:107EF00004210021002A08BF417002D0384A1268CC +:107F0000427001700020EAF7F5BC2748017800298B +:107F100004BF407870472D48D0F80401002808BFFE +:107F200070472F480068C0B27047002808BF7047EC +:107F30002DE9F0471C480078002808BFFFDF234CDC +:107F4000D4F80401002818BFBDE8F0874FF00209FB +:107F5000C4F80493234F3868C0F30018386840F021 +:107F600010003860D4F80401002804BF4FF4004525 +:107F70004FF0E02608D100BFC6F880520DF004FF94 +:107F8000D4F804010028F7D0B8F1000F03D1386805 +:107F900020F010003860C4F80893BDE8F0870B4962 +:107FA0000120886070470000C100002008F50040F3 +:107FB000001000401CF500405011004098F50140B1 +:107FC0000CF0004004F5004018F5004000F00040BF +:107FD0000000020308F501400000020204F5014020 +:107FE00000F4004010ED00E0012804BF41F6A47049 +:107FF0007047022804BF41F288307047042804BF4C +:1080000046F218007047082804BF47F2A0307047B6 +:1080100000B5FFDF41F6A47000BD10B5FE48002496 +:1080200001214470047044728472C17280F825404A +:10803000C462846380F83C4080F83D40FF2180F8B2 +:108040003E105F2180F83F1018300DF09FFFF3497C +:10805000601E0860091D0860091D0C60091D08608C +:10806000091D0C60091D0860091D0860091D0860D4 +:10807000091D0860091D0860091D0860091D0860C8 +:10808000091D0860091D086010BDE549486070477A +:10809000E448016801F00F01032904BF0120704783 +:1080A000016801F00F01042904BF02207047016834 +:1080B00001F00F01052904D0006800F00F00062828 +:1080C00007D1D948006810F0060F0CBF0820042023 +:1080D000704700B5FFDF012000BD012812BF022854 +:1080E00000207047042812BF08284FF4C87070475A +:1080F00000B5FFDF002000BD012804BF2820704725 +:10810000022804BF18207047042812BF08284FF423 +:10811000A870704700B5FFDF282000BD70B5C148CA +:10812000016801F00F01032908BF012414D0016880 +:1081300001F00F01042904BF022418210DD00168A9 +:1081400001F00F0105294BD0006800F00F00062850 +:108150001CBFFFDF012443D02821AF48C26A806AD8 +:10816000101A0E18082C04BF4EF6981547F2A030CE +:108170002DD02046042C08BF4EF628350BD0012800 +:1081800008BF41F6A47506D0022C1ABFFFDF41F6E6 +:10819000A47541F28835012C08BF41F6A47016D0B1 +:1081A000022C08BF002005D0042C1ABFFFDF0020DE +:1081B0004FF4C8702D1A022C08BF41F2883006D047 +:1081C000042C1ABFFFDF41F6A47046F21800281AEB +:1081D0004FF47A7100F2E730B0FBF1F0304470BD3B +:1081E0009148006810F0060F0CBF082404244FF4D7 +:1081F000A871B2E710B58D49026801F118040A634D +:1082000042684A63007A81F83800207E48B1207FB6 +:10821000F6F781F9A07E011C18BF0121207FF6F737 +:1082200069F9607E002808BF10BD607FF6F773F91A +:10823000E07E011C18BF0121607FBDE81040F6F709 +:1082400059B930B50024054601290AD0022908BFD2 +:108250004FF0807405D0042916BF08294FF0C74499 +:10826000FFDF44F4847040F480107149086045F4E5 +:10827000403001F1040140F00070086030BD30B5BD +:108280000024054601290AD0022908BF4FF0807456 +:1082900005D0042916BF08294FF0C744FFDF44F476 +:1082A000847040F480106249086045F4403001F168 +:1082B000040140F0007008605E48D0F8000100281A +:1082C00018BFFFDF30BD2DE9F04102274FF0E02855 +:1082D00001250024C8F88071BFF34F8FBFF36F8F63 +:1082E000554804600560FFF751F8544E18B13068E6 +:1082F00040F480603060FFF702F838B1306820F059 +:10830000690040F0960040F0004030604D494C4814 +:1083100008604FF01020806CB0F1FF3F04D04A4954 +:108320000A6860F317420A60484940F25B600860DF +:10833000091F40F203100860081F05603949032037 +:10834000086043480560444A42491160444A434931 +:108350001160121F43491160016821F440710160EE +:10836000016841F480710160C8F8807231491020C1 +:10837000C1F80403284880F83140C46228484068A6 +:10838000002808BFBDE8F081BDE8F0410047274A5A +:108390000368C2F81A308088D08302F11800017295 +:1083A00070471D4B10B51A7A8A4208D10146062241 +:1083B000981CF6F715F8002804BF012010BD002016 +:1083C00010BD154890F825007047134A5170107081 +:1083D0007047F0B50546800000F1804000F5805000 +:1083E0008B88C0F820360B78D1F8011043EA0121C0 +:1083F000C0F8001605F10800012707FA00F61A4C2C +:10840000002A04BF2068B04304D0012A18BFFFDF50 +:1084100020683043206029E0280C0020000E004036 +:10842000C40000201015004014140040100C00205F +:108430001415004000100040FC1F00403C17004095 +:108440002C000089781700408C150040381500403A +:108450005016004000000E0408F501404080004026 +:10846000A4F501401011004040160040206807FAB2 +:1084700005F108432060F0BD0CF0C4BCFE4890F844 +:1084800032007047FD4AC17811600068FC49000263 +:1084900008607047252808BF02210ED0262808BF93 +:1084A0001A210AD0272808BF502106D00A2894BFD5 +:1084B0000422062202EB4001C9B2F24A1160F249DD +:1084C000086070472DE9F047EB4CA17A012956D09E +:1084D000022918BFBDE8F087627E002A08BFBDE808 +:1084E000F087012950D0E17E667F0D1C18BF012561 +:1084F0005FF02401DFF894934FF00108C9F84C8035 +:10850000DFF88CA34718DAF80000B84228BFFFDF75 +:108510000020C9F84C01CAF80070300285F0010152 +:1085200040EA015040F0031194F82000820002F16B +:10853000804202F5C042C2F81015D64901EB800115 +:10854000A07FC20002F1804202F5F832C2F8141591 +:10855000D14BC2F81035E27FD30003F1804303F51D +:10856000F833C3F81415CD49C3F8101508FA00F014 +:1085700008FA02F10843CA490860BDE8F087227E84 +:10858000002AAED1BDE8F087A17E267F002914BF66 +:10859000012500251121ADE72DE9F041C14E8046AE +:1085A00003200D46C6F80002BD49BF4808602846B2 +:1085B0000CF02CFCB04F0124B8F1000F04BFBC72CA +:1085C000346026D0B8F1010F23D1B848006860B9F3 +:1085D00015F00C0F09D0C6F80443012000F0DAFEB4 +:1085E000F463346487F83C4002E0002000F0D2FEDF +:1085F00028460CF0F3FC0220B872FEF7B7FE38B93B +:10860000FEF7C4FE20B9AA48016841F4C021016008 +:1086100074609E48C4649E4800682946BDE8F041E5 +:1086200050E72DE9F0479F4E814603200D46C6F8DE +:108630000002DFF86C829C48C8F8000008460CF085 +:10864000E5FB28460CF0CAFC01248B4FB9F1000F62 +:1086500003D0B9F1010F0AD031E0BC72B86B40F41D +:108660008010B8634FF48010C8F8000027E00220A3 +:10867000B872B86B40F40010B8634FF40010C8F83B +:1086800000008A48006860B915F00C0F09D0C6F8E0 +:108690000443012000F07EFEF463346487F83C401C +:1086A00002E0002000F076FEFEF760FE38B9FEF72B +:1086B0006DFE20B97E48016841F4C0210160EAF7EF +:1086C000F7FA2946BDE8F047FCE62DE9F84F754C6E +:1086D0008246032088461746C4F80002DFF8C0919E +:1086E0007148C9F8000010460CF090FBDFF8C4B1E7 +:1086F000614E0125BAF1000F04BFCBF80040B572FE +:1087000004D0BAF1010F18BFFFDF2FD06A48C0F8BC +:1087100000806B4969480860B06B40F40020B0638A +:10872000D4F800321021C4F808130020C4F8000265 +:10873000DFF890C18A03CCF80020C4F80001C4F827 +:108740000C01C4F81001C4F80401C4F81401C4F801 +:1087500018015D4800680090C4F80032C9F8002094 +:10876000C4F80413BAF1010F14D026E038460CF017 +:1087700035FCFEF7FBFD38B9FEF708FE20B94C4882 +:10878000016841F4C02101605048CBF8000002208C +:10879000B072BBE74548006860B917F00C0F09D00C +:1087A000C4F80453012000F0F5FDE563256486F864 +:1087B0003C5002E0002000F0EDFD4FF40020C9F82D +:1087C00000003248C56432480068404528BFFFDFDA +:1087D00039464046BDE8F84F74E62DE9F041264C95 +:1087E0000646002594F8310017468846002808BF41 +:1087F000FFDF16B1012E16D021E094F831000128D8 +:1088000008D094F83020394640460CF014FBE16A59 +:10881000451814E094F830103A4640460CF049FBF5 +:10882000E16A45180BE094F8310094F83010012803 +:108830003A46404609D00CF064FBE16A45183A46D6 +:1088400029463046BDE8F0413FE70CF014FBE16AF1 +:108850004518F4E72DE9F84F124CD4F8000220F047 +:108860000309D4F804034FF0100AC0F30018C4F849 +:1088700008A300262CE00000280C0020241500404E +:108880001C150040081500405415004000800040B1 +:108890004C850040006000404C81004010110040B9 +:1088A00004F5014000100040000004048817004057 +:1088B00068150040ACF50140488500404881004003 +:1088C000A8F5014008F501401811004004100040CF +:1088D000C4F80062FC48FB490160FC4D0127A97AFD +:1088E000012902D0022903D015E0297E11B912E036 +:1088F000697E81B1A97FEA7F07FA01F107FA02F2E6 +:108900001143016095F82000800000F1804000F5DF +:10891000C040C0F81065FF208DF80000C4F8106159 +:10892000276104E09DF80000401E8DF800009DF8CE +:10893000000018B1D4F810010028F3D09DF8000011 +:10894000002808BFFFDFC4F81061002000F022FDFE +:108950006E72AE72EF72C4F80092B8F1000F18BFD9 +:10896000C4F804A3BDE8F88FFF2008B58DF8000017 +:10897000D7480021C0F810110121016105E000BFB6 +:108980009DF80010491E8DF800109DF8001019B1D7 +:10899000D0F810110029F3D09DF80000002808BF7E +:1089A000FFDF08BD0068CB4920F07F4008607047BA +:1089B0004FF0E0200221C0F8801100F5C070BFF335 +:1089C0004F8FBFF36F8FC0F80011704710B490E85D +:1089D0001C10C14981E81C10D0E90420C1E9042021 +:1089E00010BC70474FF0E0210220C1F80001704731 +:1089F000BA4908707047BA490860704770B50546B3 +:108A0000EAF756F9B14C2844E16A884298BFFFDF83 +:108A100001202074EAF74CF9B24A28440021606131 +:108A2000C2F84411B0490860A06BB04940F480001E +:108A3000A063D001086070BD70B5A44C0546AC4A77 +:108A40000220207410680E4600F00F00032808BFB3 +:108A5000012213D0106800F00F00042808BF022282 +:108A60000CD0106800F00F0005281BD0106800F033 +:108A70000F0006281CBFFFDF012213D094F831003D +:108A800094F83010012815D028460CF081FA954949 +:108A900060610020C1F844016169E06A08449249BC +:108AA000086070BD9348006810F0060F0CBF0822E4 +:108AB0000422E3E7334628460CF038FAE7E7824918 +:108AC0004FF4800008608148816B21F4800181634C +:108AD000002101747047C20002F1804202F5F832B1 +:108AE000854BC2F81035C2F81415012181407F482A +:108AF00001607648826B1143816370477948012198 +:108B00004160C1600021C0F84411774801606F489E +:108B1000C1627047794908606D48D0F8001241F091 +:108B20004001C0F8001270476948D0F8001221F0E7 +:108B30004001C0F800127149002008607047644885 +:108B4000D0F8001221F01001C0F80012012181615B +:108B500070475E49FF2081F83E005D480021C0F863 +:108B60001C11D0F8001241F01001C0F8001270473B +:108B7000574981B0D1F81C21012A0DD0534991F8F1 +:108B80003E10FF290DBF00204942017001B008BF0F +:108B90007047012001B07047594A126802F07F0205 +:108BA000524202700020C1F81C0156480068009033 +:108BB000EFE7F0B517460C00064608BFFFDF434D50 +:108BC00014F0010F2F731CBF012CFFDF002E0CBF10 +:108BD000012002206872EC7201281CBF0228FFDF0E +:108BE000F0BD3A4981F83F007047384A136C036082 +:108BF000506C086070472DE9F84F38480078042819 +:108C000028BFFFDF314CDFF8C080314D94F83C00C5 +:108C100000260127E0B1D5F8040110F1000918BFC2 +:108C20004FF00109D5F81001002818BF012050EAC3 +:108C300009014FF4002B17D08021C5F80813C8F89C +:108C400000B084F83C6090F0010F18BFBDE8F88FC9 +:108C5000DFF89090D9F84C01002871D0A07A012853 +:108C60006FD002286ED0D1E0D5F80001DFF890A0D7 +:108C700030B3C5F800616F61FF20009002E0401E34 +:108C8000009005D0D5F81C0100280098F7D000B955 +:108C9000FFDFDAF8000000F07F0A94F83F0050454B +:108CA0003CBF002000F076FB84F83EA0C5F81C61B4 +:108CB000C5F808731348006800902F64AF6326E07E +:108CC00022E0000000000E0408F50140280C0020FE +:108CD000001000403C150040100C0020C400002093 +:108CE00004150040008000404485004004F5014028 +:108CF000101500401414004004110040601500409D +:108D0000481500401C110040B9F1000F03D0B9F123 +:108D1000000F2ED05CE0DAF8000000F07F0084F84D +:108D20003E00C5F81C6194F83D1061B194F83F1005 +:108D300081421BD2002000F02DFB2F64AF6315E0B1 +:108D400064E04CE04EE0FE49096894F83F308AB296 +:108D5000090C984203D30F2A06D9022904D2012014 +:108D600000F018FB2F6401E02F64AF63F548006842 +:108D700000908022C5F80423F3498F64F348036808 +:108D8000A0F1040CDCF800C043F698273B4463458F +:108D900015D2026842F210731A440260C1F84861A9 +:108DA000EC49EB480860091FEB480860EB48C0F845 +:108DB00000B0A06B40F40020A063BDE8F88F06600F +:108DC000C1F84861C5F80823C8F800B0C1F8486187 +:108DD0008020C5F80803C8F800B0BDE8F88F207EF1 +:108DE00010B913E0607E88B1A07FE17F07FA00F040 +:108DF00007FA01F10843C8F8000094F82000800049 +:108E000000F1804000F5C040C0F81065C9F84C7012 +:108E1000D34800682064D34800686064D248A16BDE +:108E20000160A663217C002019B1D9F84411012901 +:108E300000D00021A27A012A6ED0022A74D000BF8D +:108E4000D5F8101101290CBF1021002141EA0008BA +:108E5000C648016811F0FF0F03D0D5F8141101299D +:108E600000D0002184F83210006810F0FF0F03D00A +:108E7000D5F81801012800D0002084F83300BC4840 +:108E8000006884F83400FEF774FF012818BF002042 +:108E900084F83500C5F80061C5F80C61C5F81061AB +:108EA000C5F80461C5F81461C5F81861B1480068D7 +:108EB0000090A548C0F84461AF480068DFF8BC9254 +:108EC0000090D9F80000A062A9F104000068E062F7 +:108ED000AB48016801F00F01032908BF012013D03E +:108EE000016801F00F01042908BF02200CD00168BD +:108EF00001F00F01052926D0006800F00F000628B8 +:108F00001CBFFFDF01201ED084F83000A07A84F857 +:108F1000310002282CD11EE0D5F80C01012814BF25 +:108F2000002008208CE7FFE7D5F80C01012814BFCA +:108F300000200220934A1268012A14BF0422002252 +:108F4000104308437CE79048006810F0060F0CBF00 +:108F500008200420D8E7607850B18C490968097866 +:108F60000840217831EA000008BF84F8247001D05D +:108F700084F82460DFF818A218F0020F06D0E9F791 +:108F800097FEA16A081ADAF81010884718F0010F46 +:108F900018BF4FF0000B0DD0E9F78AFEE16ADAF84E +:108FA0001420081A594690477A48007810F0010FAB +:108FB0002FD10CE018F0020F18BF4FF0010BEBD1CE +:108FC00018F0080F18BF4FF0020BE5D1ECE7DFF8FF +:108FD000BCB1DBF80000007800F00F00072828BFC4 +:108FE00084F8256015D2DBF80000062200F10901A3 +:108FF000A01CF5F7F5F940B9207ADBF800100978E4 +:10900000B0EBD11F08BF012001D04FF0000084F861 +:109010002500E17A4FF0000011F0020F1CBF18F09C +:10902000020F18F0040F19D111F0100F1CBF94F8A3 +:109030003320002A02D094F835207AB111F0080FBD +:109040001CBF94F82420002A08D111F0040F02D08C +:1090500094F8251011B118F0010F01D04FF0010064 +:10906000617A19B168B1FFF7F5FB10E03E484A4953 +:109070000160D5F8000220F00300C5F80002E77295 +:1090800005E001290AD0022918BFFFDF0DD018F032 +:10909000010F14D0DAF80000804745E06672E772ED +:1090A000A7729621227B002006E06672E7720220FA +:1090B000A072227B96210120FFF78FFBE7E718F0D3 +:1090C000020F2AD018F0040F21D1FEF74FF9F0B9A2 +:1090D000FEF75CF9D8B931480168001F0068C0F399 +:1090E000006CC0F3425500F00F03C0F30312C0F34D +:1090F0000320BCF1000F0AD0002B1CBF002A00285F +:1091000005D1002918BF032D38BF48F0040827EA0D +:109110009800DAF80410884706E018F0080F18BF26 +:10912000DAF8080056D08047A07A022818BFBDE8B8 +:10913000F88F207C002808BFBDE8F88F02492FE097 +:10914000741500401C11004000800040488500401C +:1091500014100040ACF501404881004004F5014086 +:1091600004B500404C85004008F501404016004021 +:109170001014004018110040448100404485004014 +:109180001015004000140040141400400415004065 +:10919000100C0020C40000200000040454140040FF +:1091A000C1F8446102281DD0012818BFFFDFE16A21 +:1091B0006069884298BFFFDFD4F81400C9F8000046 +:1091C000A06B4FF4800140F48000A06382480160EE +:1091D000BDE8F88F18F0100F14BFDAF80C00FFDFAD +:1091E000A1D1A1E76169E06A0844E7E738B57B49A6 +:1091F00004460220887201212046FFF763F9784A6D +:1092000004F13D001060774B0020C3F8440176491B +:10921000C1F80001C1F80C01C1F81001C1F8040146 +:10922000C1F81401C1F818017048006800900120CD +:109230009864101D00681168884228BFFFDF38BDA0 +:109240002DE9F843654A88460024917A0125684F44 +:10925000012902D0022903D015E0117E11B912E0D4 +:10926000517E81B1917FD37F05FA01F105FA03F3B5 +:109270001943396092F82010890001F1804101F50D +:10928000C041C1F8104506460220907201213046C7 +:10929000FFF718F9524906F13D000860514AC2F83B +:1092A00044415148C0F80041C0F80C41C0F8104199 +:1092B000C0F80441C0F81441C0F818414B48006898 +:1092C00000909564081D00680968884228BFFFDF88 +:1092D000B8F1000F1CBF4FF400303860BDE8F883D0 +:1092E000022810B50DD0012804BF42F6CE3010BDC3 +:1092F000042817BF082843F6A440FFDF41F66A00A0 +:1093000010BDFDF7E5FF30B9FDF7F9FF002808BFF4 +:1093100041F6583001D041F2643041F29A010844DC +:1093200010BD314910B50020C1F800023049314864 +:109330000860324930480860091D31480860091D3D +:1093400030480860091D30480860091D2F48086032 +:10935000091D2F48086001200BF058FD1E494FF4ED +:109360003810086010BD22494FF43810086070476B +:109370002848016803291BBF00680228012000203B +:109380007047244801680B291BBF00680A28012088 +:109390000020704720490968C9B9204A204913684C +:1093A00070B123F0820343F07D0343F00043136068 +:1093B0000A6822F0100242F0600242F0004205E02A +:1093C00023F0004313600A6822F000420A60034958 +:1093D00081F83D007047000004F50140280C002092 +:1093E00044850040008000400010004018110040FB +:1093F00008F50140000004041011004098F50140F8 +:109400000410004044810040141000401C11004032 +:109410001010004050150040881700403C170040D5 +:109420007C17004010B5404822220021F5F72FF8A4 +:109430003D480024017821F010010170012104F061 +:10944000FFFE3A494FF6FF7081F822408884384980 +:109450000880488010BD704734498A8C824218BF0A +:109460007047002081F822004FF6FF708884704713 +:109470002D49016070472E49088070472B498A8C1E +:10948000A2F57F43FF3B03D00021016008467047EF +:1094900091F822202549012A1ABF016001200020ED +:1094A0007047224901F1220091F82220012A04BFCD +:1094B00000207047012202701D48008888841046F1 +:1094C00070471B49488070471849194B8A8C5B8844 +:1094D0009A4206D191F82220002A1EBF0160012085 +:1094E0007047002070471148114A818C5288914280 +:1094F00009D14FF6FF71818410F8221F19B10021A4 +:10950000017001207047002070470848084A818C8C +:109510005288914205D190F8220000281CBF0020FB +:109520007047012070470000960C0020700C00204E +:10953000CC0000207047584A012340B1012818BFD1 +:1095400070471370086890608888908170475370E6 +:109550000868C2F802008888D08070474E4A10B16F +:10956000012807D00EE0507860B1D2F80200086000 +:10957000D08804E0107828B19068086090898880CD +:109580000120704700207047434910B1012803D0E3 +:1095900006E0487810B903E0087808B10120704768 +:1095A0000020704730B58DB00C4605460D220021D5 +:1095B00004A8F4F76CFFE0788DF81F0020798DF88F +:1095C0001E0060798DF81D00286800906868019081 +:1095D000A8680290E868039068460AF087FF207840 +:1095E0009DF82F1088420CD160789DF82E1088428B +:1095F00007D1A0789DF82D10884202BF01200DB040 +:1096000030BD00200DB030BD30B50C4605468DB0E4 +:109610004FF0030104F1030012B1FDF749FF01E02F +:10962000FDF765FF60790D2220F0C00040F040009A +:109630006071002104A8F4F72AFFE0788DF81F007C +:1096400020798DF81E0060798DF81D002868009043 +:1096500068680190A8680290E868039068460AF07C +:1096600045FF9DF82F0020709DF82E0060709DF83A +:109670002D00A0700DB030BD10B5002904464FF08C +:10968000060102D0FDF714FF01E0FDF730FF60791D +:1096900020F0C000607110BDD0000020FE4840687E +:1096A00070472DE9F0410F46064601461446012059 +:1096B00005F06FF8054696F86500FEF795FC4AF24E +:1096C000B12108444FF47A71B0FBF1F0718840F297 +:1096D00071225143C0EB4100001BA0F55A7402F007 +:1096E0005AFF002818BF1E3CAF4234BF28463846F8 +:1096F000A04203D2AF422CBF3C462C467462BDE868 +:10970000F0812DE9FF4F8BB0044690F86500884644 +:109710000390DDE90D1008430A90E0480027057822 +:109720000C2D28BFFFDFDE4E36F8159094F88851D7 +:109730000C2D28BFFFDFDA4830F81500484480B20E +:10974000009094F87D000D280CBF012000200790A8 +:109750000D98002804BF94F82C0103282BD10798FA +:1097600048B3B4F8AA01404525D1D4F83401C4F86F +:109770002001608840F2E2414843C4F82401B4F873 +:109780007A01B4F806110844C4F82801204602F012 +:109790000CFFB4F8AE01E08294F8AC016075B4F847 +:1097A000B0016080B4F8B201A080B4F8B401E080E8 +:1097B000022084F82C01D4F884010990D4F88001A7 +:1097C0000690B4F80661B4F87801D4F874110191E8 +:1097D0000D9921B194F8401151B100F0D6B804F5BB +:1097E000807104917431059104F5B075091D07E08D +:1097F00004F5AA710491091D059104F5A275091DCE +:109800000891B4F87010A8EB0000A8EB01010FFA62 +:1098100080F90FFA81FBB9F1000F05DAD4F8700175 +:1098200001900120D9460A909C484FF0000A007927 +:10983000A8B3F2F77FFB90B3B4F8180102282ED337 +:1098400094F82C0102282AD094F8430138BB94F8EC +:10985000880100900C2828BFFFDF9148009930F85C +:10986000110000F5C86080B2009094F82C01012826 +:109870007ED0608840F2E2414843009901F0E6F86A +:10988000D4F8342180B206EB0B01A1EB0901821A56 +:1098900001FB02AAC4F83401012084F8430194F8C2 +:1098A0002C01002865D0012800F01482022800F065 +:1098B0007181032818BFFFDF00F04782A7EB0A0180 +:1098C0000198FCF738F90599012640F271220860E9 +:1098D0000898A0F80080002028702E710598006874 +:1098E000A8606188D4F834015143C0EB41006B4952 +:1098F000A0F54E70C8618969814287BF04990860EC +:10990000049801600498616A0068084400F5D47006 +:10991000E86002F040FE10B1E8681E30E8606E7149 +:10992000B4F8F000A0EB080000B20028C4BF032088 +:109930006871079800280E9800F06982E0B100BFB6 +:10994000B4F8181100290CBF0020B4F81A01A4F8CB +:109950001A0194F81C21401C504388420CD26879AB +:10996000401E002808DD6E71B4F81A01401C01E0A9 +:109970000FE05AE0A4F81A010D98002800F06A825E +:1099800094F84001002800F061820FB00220BDE889 +:10999000F08F94F8800003283DD03F4894F865107C +:1099A00090F82C00F7F78DFDE18A40F271225143C7 +:1099B00000EB4100CDF80800D4F82401009901F033 +:1099C00045F8D4F82021D4F82811821A01FB02AA04 +:1099D000C4F820010099029801F038F8D4F8301149 +:1099E000C4F83001411A8A44608840F2E241484399 +:1099F000009901F02BF806EB0B01D4F82821A1EB1C +:109A00000901891AD4F83421C4F83401821A491E94 +:109A100001FB02AA40E7E18A40F27122D4F8240156 +:109A2000514300EB41000290C6E70698002808BFAA +:109A3000FFDF94F86510184890F82C00F7F741FD07 +:109A40000990E08A40F271214143099800EB4100FE +:109A5000009900F0FBFFC4F83001608840F2E24159 +:109A60004843009900F0F2FFC4F8340103A902A8AA +:109A7000FFF7BBF8DDE90160039FE9F7F0F8014665 +:109A80003046FDF769F800F10F06E9F711F9381AC9 +:109A9000801B009006E00000B80C0020E0000020D1 +:109AA000E4620200B4F83401214686B20120D4F801 +:109AB000289004F06EFE074694F86500FEF794FACD +:109AC0004AF2B12108444FF47A7BB0FBFBF0618885 +:109AD00040F271225143C0EB4100801BA0F55A7641 +:109AE00002F059FD002818BF1E3EB94534BF384664 +:109AF0004846B04203D2B9452CBF4E463E46666248 +:109B000094F86500FEF7E9FA00F2E140B0FBFBF1E2 +:109B100006980F1894F86500FEF7DFFA064694F8E9 +:109B20006500FEF761FA30444AF2AB310844B0FBFD +:109B3000FBF1E08A40F2712242430998D4F8306187 +:109B400000EB4200401A801B384400993138471A14 +:109B5000607D40F2E24110FB01F994F8650000904D +:109B600010F00C0F0ABF00984EF62830FEF73CFAB2 +:109B70004AF2B1210844B0FBFBF000EB460000EBD9 +:109B800009060098FEF7B8FA304400F18401FE4857 +:109B9000816193E6E18A40F27122D4F824015143B5 +:109BA00000EB4100009900F051FFC4F830016188DA +:109BB00040F2E2404843009900F048FFC4F8340105 +:109BC00087B221460120D4F828B004F0E2FD814696 +:109BD00094F86500FEF708FA4AF2B12101444FF407 +:109BE0007A70B1FBF0F0618840F271225143C0EB12 +:109BF0004100C01BA0F55A7702F0CDFC002818BF29 +:109C00001E3FCB4534BF48465846B84203D2CB45E9 +:109C10002CBF5F464F4667621EBB0E9808B394F890 +:109C200065603046FEF7E0F94AF2B12101444FF495 +:109C30007A70B1FBF0F0D4F83011E28A084440F2B7 +:109C40007123D4F824115A4301EB42010F1A304614 +:109C5000FEF752FA01460998401A3844A0F120074D +:109C60000AE0E18A40F27122D4F82401514300EB6A +:109C70004100D4F83011471AD4F82821D4F8201123 +:109C8000D4F8300101FB0209607D40F2E24110FB93 +:109C900001FB94F8656016F00C0F0ABF30464EF6D3 +:109CA0002830FEF7A1F94AF2B12101444FF47A704D +:109CB000B1FBF0F000EB490000EB0B093046FEF77A +:109CC0001BFA484400F16001AF488161012084F82B +:109CD0002C01F3E5618840F271225143D4F834013C +:109CE000D4F82821C0EB410101FB09F706EB0B0179 +:109CF000891AD4F820C1D4F83031491E0CFB023245 +:109D000001FB0029607D40F2E24110FB01FB94F869 +:109D1000656016F00C0F0ABF30464EF62830FEF78D +:109D200063F94AF2B12101444FF47A70B1FBF0F0CB +:109D300000EB490000EB0B093046FEF7DDF9484423 +:109D400000F1600190488161B8E5618840F27122BC +:109D5000D4F834015143C0EB410000FB09F794F8FB +:109D60007C0024281CBF94F87D0024280BD1B4F873 +:109D7000AA01A8EB000000B2002804DB94F8AD01B2 +:109D8000002818BF03900A9800B3FEB9099800286C +:109D90001ABF06980028FFDF94F8650010F00C0F3A +:109DA00014BF4EF62830FEF71FF94AF2B1210144E4 +:109DB0004FF47A70B1FBF0F03F1A94F86500FEF7AB +:109DC0009BF90999081A3844A0F12007D4F83411F6 +:109DD00006EB0B0000FB01F6039810F00C0F0ABF16 +:109DE00003984EF62830FEF7FFF84AF2B1210144FD +:109DF0004FF47A70B1FBF0F000EB46060398FEF7E3 +:109E00007BF9304400F160015F48816156E500282C +:109E10007FF496AD94F82C0100283FF4ADAD618835 +:109E200040F27122D4F834015143C0EB410128467D +:109E3000F7F712F90004000C3FF49EAD18990029C1 +:109E400018BF088001200FB0BDE8F08F94F87C01A6 +:109E5000FCF7D1FC94F87C012946FCF7B0FB20B15B +:109E60000D9880F0010084F841010FB00020BDE89A +:109E7000F08F2DE9F843454C0246434F00266168B8 +:109E8000606A052A60D2DFE802F003464B4F5600B5 +:109E9000A07A002560B101216846FDF709FB9DF815 +:109EA000000042F210710002B0FBF1F201FB12055A +:109EB000F4F720FE4119A069FBF73DFEA06126746E +:109EC000032060754FF0010884F81480607AD0B9DF +:109ED000A06A80B1F4F70EFE0544F4F785FC411941 +:109EE000A06A884224BF401BA06204D2C4F8288024 +:109EF000F5F72BFE07E0207B04F11001FCF75FFB78 +:109F0000002808BFFFDF2684FCF739F87879BDE820 +:109F1000F843E8F7F9BFBDE8F843002100F0B3BD0E +:109F2000C1F88001BDE8F883D1F88001BDE8F843AD +:109F3000012100F0A8BD84F83060FCF720F87879A2 +:109F4000BDE8F843E8F7E0BFFFDFBDE8F8832DE99F +:109F5000F04F0E4C824683B020788B4601270025B7 +:109F6000094E4FF00209032804BF207B50457DD1E4 +:109F7000606870612078032818BFFFDF4FF0030886 +:109F8000BBF1080F73D203E0E0000020B80C002002 +:109F9000DFE80BF0040F32322D9999926562F5F7E4 +:109FA000ABF9002818BFFFDF86F8028003B0BDE8D8 +:109FB000F08FF4F77AFF68B9F4F716FC0546E0690C +:109FC000A84228BFE56105D2281A0421FCF7F7FD55 +:109FD000E56138B1F5F723FD002818BFFFDF03B0B6 +:109FE000BDE8F08F03B00020BDE8F04F41E703B0BB +:109FF000BDE8F04FFEF7FFBD2775257494F83000DB +:10A000004FF0010A58B14FF47A71A069FBF793FD44 +:10A01000A061002104F11000F7F71EF80EE0F4F73C +:10A0200069FD82465146A069FBF785FDA061514656 +:10A0300004F11000F7F710F800F1010A208C411C20 +:10A040000A293CBF50442084606830B1208C401CF9 +:10A050000A2828BF84F8159001D284F81580607A08 +:10A06000A8B9A06AE8B1F4F745FD01E02FE02AE0C5 +:10A070008046F4F7B9FB00EB0801A06A884224BFD0 +:10A08000A0EB0800A0620CD2A762F5F75EFD207B72 +:10A09000FCF74BF82570707903B0BDE8F04FE8F796 +:10A0A00033BF207B04F11001FCF789FA002808BFB8 +:10A0B000FFDF03B0BDE8F08F207BFCF736F825709A +:10A0C00003B0BDE8F08FFFDF03B0BDE8F08FBAF159 +:10A0D000200F28BFFFDFDFF8E886072138F81A00D5 +:10A0E000F9F7E4FE040008BFFFDFBAF1200F28BF34 +:10A0F000FFDF38F81A002188884218BFFFDF4FF0D1 +:10A10000200A7461BBF1080F80F06181DFE80BF079 +:10A110000496A0A099FEFDFCC4F88051F580C4F817 +:10A12000845194F8410138B9FCF71EF8D4F84C1169 +:10A13000FCF712FD00281BDCB4F83E11B4F87000E7 +:10A14000814206D1B4F8F410081AA4F8F6002046AB +:10A1500005E0081AA4F8F600B4F83E112046A4F869 +:10A160007010D4F86811C4F84C11C0F870111DE0DB +:10A17000B4F83C11B4F87000081AA4F8F600B4F86A +:10A180003C112046A4F87010D4F84C11C4F86811A2 +:10A19000C4F87011D4F85411C4F80011D4F858114F +:10A1A000C4F87411B4F85C11A4F8781102F008F93D +:10A1B000FBF7B4FF804694F86500FDF715FF4AF2FF +:10A1C000B12108444FF47A71B0FBF1F0D4F83411A6 +:10A1D00040F27122084461885143C0EB4100A0F174 +:10A1E000300AB8F1B70F98BF4FF0B70821460120E9 +:10A1F00004F0CFFA4044AAEB0000A0F21D38A246BA +:10A200002146012004F0C5FADAF824109C3081427E +:10A2100088BF0D1AC6F80C80454528BF4546B56075 +:10A22000D4F86C01A0F5D4703061FCF762FC84F8BE +:10A23000407186F8029003B0BDE8F08F02F0A6F9F5 +:10A2400001E0FEF7D8FC84F8407103B0BDE8F08F60 +:10A25000FBF78AFFD4F8702101461046FCF77CFC1E +:10A2600048B1628840F27123D4F834115A43C1EBEB +:10A270004201B0FBF1F094F87D100D290FD0B4F835 +:10A280007010B4F83E210B189A42AEBF501C401C0F +:10A290000844A4F83E0194F8420178B905E0B4F806 +:10A2A0003E01401CA4F83E0108E0B4F83E01B4F8B9 +:10A2B000F410884204BF401CA4F83E01B4F87A01AF +:10A2C0000DF1040B401CA4F87A01B4F89A00B4F81C +:10A2D0009810401AB4F87010401E08441FFA80F914 +:10A2E0000BE000231A462046CDF800B0FFF709FA2C +:10A2F00068B3012818BFFFDF48D0B4F83E11A9EBBE +:10A30000010000B2002802E053E047E05FE0E8DA35 +:10A31000082084F88D0084F88C70204601F012FE2D +:10A3200084F82C5194F87C514FF6FF77202D00D300 +:10A33000FFDF28F8157094F87C01FBF7F6FE84F82F +:10A340007CA1707903B0BDE8F04FE8F7DDBDA06EE9 +:10A35000002804BF03B0BDE8F08FB4F83E01B4F8A4 +:10A360009420801A01B20029DCBF03B0BDE8F08F51 +:10A37000B4F86C000144491E91FBF0F189B201FB75 +:10A380000020A4F8940003B0BDE8F08FB4F83E01BB +:10A39000BDF804100844A4F83E01AEE7FEF7E4FA65 +:10A3A000FEF729FC4FF0E020C0F8809203B0BDE832 +:10A3B000F08F94F82C01042818BFFFDF84F82C518B +:10A3C00094F87C514FF6FF77202DB2D3B0E7FFDF32 +:10A3D00003B0BDE8F08F10B5FA4C207850B10120E1 +:10A3E0006072F5F7D8FB2078032805D0207A002882 +:10A3F00008BF10BD0C2010BD207BFCF7FCF9207BB2 +:10A40000FCF765FC207BFBF790FE002808BFFFDF10 +:10A410000020207010BD2DE9F04FEA4F83B038784E +:10A4200001244FF0000840B17C720120F5F7B3FB26 +:10A430003878032818BF387A0DD0DFF88C9389F864 +:10A44000034069460720F9F7BAFC002818BFFFDF70 +:10A450004FF6FF7440E0387BFCF7CDF9387BFCF712 +:10A4600036FC387BFBF761FE002808BFFFDF87F86A +:10A470000080E2E7029800281CBF90F82C11002908 +:10A480002AD00088A0421CBFDFF834A34FF0200B75 +:10A490003AD00721F9F70AFD040008BFFFDF94F85E +:10A4A0007C01FCF714FC84F82C8194F87C514FF665 +:10A4B000FF76202D28BFFFDF2AF8156094F87C0175 +:10A4C000FBF733FE84F87CB169460720F9F777FC87 +:10A4D000002818BFFFDF12E06846F9F74EFC00289D +:10A4E000C8D011E0029800281CBF90F82C11002958 +:10A4F00005D00088A0F57F41FF39CAD104E0684645 +:10A50000F9F73BFC0028EDD089F8038087F830800C +:10A5100087F80B8003B00020BDE8F08FAA4948718E +:10A520000020887001220A7048700A71C870A5491D +:10A53000087070E7A449087070472DE9F84FA14CE6 +:10A5400006460F462078002862D1A048FBF792FD0E +:10A55000207320285CD04FF00308666084F80080E8 +:10A56000002565722572AEB1012106F58E70FCF7EB +:10A57000BEFF0620F9F742FC81460720F9F73EFCB2 +:10A5800096F81C114844B1FBF0F200FB1210401C7D +:10A5900086F81C01FBF7C2FD40F2F651884238BF35 +:10A5A00040F2F65000F5A0701FFA80F9F4F7A2FA15 +:10A5B000012680B3A672F4F717F9E061FBF7D4FD2A +:10A5C000824601216846FCF769FF9DF8000042F2CF +:10A5D00010710002B0FBF1F201FB120000EB090167 +:10A5E0005046FBF7A8FAA762A061267584F815808B +:10A5F0002574207B04F11001FBF7E1FF002808BF60 +:10A60000FFDF25840020F5F7C6FA0020BDE8F88FAB +:10A610000C20BDE8F88FFFE7E761FBF7A5FD494691 +:10A62000FBF789FAA061A57284F830600120FDF77C +:10A6300054FD4FF47A7100F2E140B0FBF1F0381AAA +:10A64000A0F5AB60A5626063CFE75F4948707047D3 +:10A650005D49087170475B4810B5417A00291CBFFD +:10A66000002010BD816A51B990F8301039B1416AAB +:10A67000406B814203D9F5F768FA002010BD012034 +:10A6800010BD2DE9F041504C0646E088401CE080AA +:10A69000D4E902516078D6F8807120B13A46284654 +:10A6A000F6F705FD0546A068854205D02169281A00 +:10A6B00008442061FCF71DFAA560AF4209D896F85E +:10A6C0002C01012805D0E078002804BF0120BDE856 +:10A6D000F0810020BDE8F08110B504460846FDF782 +:10A6E00083FC4AF2B12108444FF47A71B0FBF1F0D7 +:10A6F00040F2E241614300F54E7081428CBF081A7E +:10A70000002010BD70B5044682B0002084F84001DE +:10A7100094F8FB00002807BF94F82C01032802B02E +:10A7200070BDFBF721FDD4F8702101461046FCF7FF +:10A7300013FA0028DCBF02B070BD628840F27123BA +:10A74000D4F834115A43C1EB4201B0FBF1F0B4F834 +:10A750007010401C0844A4F83C01B4F8F400B4F8AC +:10A760003C21801A00B20028DCBF02B070BD01207D +:10A7700084F84201B4F89A00B4F8982001AE801A27 +:10A78000401E084485B212E00096B4F83C11002344 +:10A7900001222046FEF7B5FF002804BF02B070BDBD +:10A7A000012815D0022812BFFFDF02B070BDB4F837 +:10A7B0003C01281A00B20028BCBF02B070BDE3E71C +:10A7C000F00C0020B80C0020E00000204F9F01009A +:10A7D000B4F83C01BDF804100844A4F83C01E6E7D5 +:10A7E000F8B50422002506295BD2DFE801F0072630 +:10A7F0000319192A044680F82C2107E00446C948A9 +:10A80000C078002818BF84F82C210AD0FBF7B7FBCA +:10A81000A4F87A51B4F87000A4F83E0184F84251CB +:10A82000F8BD0095B4F8F410012300222046FEF78D +:10A8300068FF002818BFFFDFE8E7032180F82C112C +:10A84000F8BD0646876AB0F83401314685B201206A +:10A8500003F09FFF044696F86500FDF7C5FB4AF23A +:10A86000B12108444FF47A71B0FBF1F0718840F2E5 +:10A8700071225143C0EB4100401BA0F55A7501F015 +:10A880008AFE002818BF1E3DA74234BF2046384626 +:10A89000A84228BF2C4602D2A74228BF3C46746279 +:10A8A000F8BDFFDFF8BD2DE9F05F9E4EB1780229BB +:10A8B00006BFF1880029BDE8F09F7469C4F88401DF +:10A8C00094F86500FDF718FCD4F88411081AB168F3 +:10A8D0000144B160F1680844F060746994F8430180 +:10A8E000002808BFBDE8F09F94F82C01032818BF8A +:10A8F000BDE8F09F94F8655037780C2F28BFFFDF34 +:10A90000894E36F8178094F888710C2F28BFFFDF26 +:10A9100036F81700404494F8888187B2B8F10C0FDC +:10A9200028BFFFDF36F8180000F5C8601FFA80F86E +:10A930002846FDF7E1FBD4F884114FF0000A0E1A07 +:10A9400015F00C0F0ABF28464EF62830FDF74CFBD9 +:10A950004FF47A7900F2E730B0FBF9F0361A284666 +:10A96000FDF7CAFBD4F8001115F00C0FA1EB000B9A +:10A970000ABF28464EF62830FDF736FB4AF2B121D1 +:10A980000844B0FBF9F0ABEB0000A0F160017943A3 +:10A99000B1FBF8F1292202EB50006031A0EB51022B +:10A9A00000EB5100B24201D8B04201D8F1F774FB7C +:10A9B000608840F2E2414843394600F047F8C4F865 +:10A9C000340184F843A1BDE8F09F70B505465548B1 +:10A9D00090F802C0BCF1020F07BF406900F5C074D7 +:10A9E000524800F12404002904BF256070BD4FF4D3 +:10A9F0007A7601290DD002291CBFFFDF70BD1046F9 +:10AA0000FEF76EFC00F2E140B0FBF6F0281A206081 +:10AA100070BD1846FDF761FB00F2E140B0FBF6F0B7 +:10AA2000281A206070BD4148007800281CBF002013 +:10AA3000704710B50720F9F7D3F980F0010010BD79 +:10AA40003A480078002818BF0120704730B5024608 +:10AA50000020002908BF30BDA2FB0110490A41EACD +:10AA6000C051400A4C1C40F100000022D4F1FF31DB +:10AA700040F2A17572EB000038BFFFDF04F5F4600F +:10AA8000B0FBF5F030BD2DE9F843284C0025814698 +:10AA900084F83050D4F8188084F82C10E5722570B2 +:10AAA0000127277239466068F5F792FD6168C1F8A1 +:10AAB0007081267B81F87C61C1F88091C1F8748136 +:10AAC000B1F80080202E28BFFFDF194820F816803B +:10AAD000646884F82C510023A4F878511A4619466A +:10AAE00020460095FEF70DFE002818BFFFDFC4F8D2 +:10AAF0002851C4F8205184F82C71A4F83E51A4F8D0 +:10AB00003C5184F84251B4F87000401EA4F8700023 +:10AB1000A4F87A51FBF733FA02484079BDE8F843CC +:10AB2000E8F7F2B9E0000020E4620200B80C00206F +:10AB3000F00C0020012804D0022805D0032808D1F9 +:10AB400005E0012907D004E0022904D001E004292E +:10AB500001D000207047012070472DE9F0410E46DA +:10AB6000044603F08AFC0546204603F08AFC0446AE +:10AB7000F6F770FBFB4F010015D0386990F86420A0 +:10AB80008A4210D090F8C0311BB190F8C2312342F4 +:10AB90001FD02EB990F85D30234201D18A4218D8D7 +:10ABA00090F8C001A8B12846F6F754FB70B1396996 +:10ABB00091F86520824209D091F8C00118B191F84E +:10ABC000C301284205D091F8C00110B10120BDE8B1 +:10ABD000F0810020FBE730B5E24C85B0E069002849 +:10ABE0005FD0142200216846F3F751FC206990F8E9 +:10ABF0006500FDF7F9F94FF47A7100F5FA70B0FBD2 +:10AC0000F1F5206990F86500FDF776FA2844ADF873 +:10AC1000060020690188ADF80010B0F87010ADF89A +:10AC200004104188ADF8021090F8A20130B1A0697B +:10AC3000C11C039103F002FB8DF81000206990F80D +:10AC4000A1018DF80800E169684688472069002164 +:10AC500080F8A21180F8A1110399002921D090F861 +:10AC6000A01100291DD190F87C10272919D09DF83A +:10AC70001010039A002914D013780124FF2B12D04E +:10AC8000072B0ED102290CD15178FF2909D100BF21 +:10AC900080F8A0410399C0F8A4119DF8101080F825 +:10ACA000A31105B030BD1B29F2D9FAE770B5AD4C40 +:10ACB000206990F87D001B2800D0FFDF2069002567 +:10ACC00080F8A75090F8D40100B1FFDF206990F818 +:10ACD000A81041B180F8A8500188A0F8D81180F8D8 +:10ACE000D6510E2108E00188A0F8D81180F8D6517D +:10ACF000012180F8DA110D2180F8D4110088F9F7CC +:10AD000006FAF8F79FFE2079E8F7FEF8206980F848 +:10AD10007D5070BD70B5934CA07980072CD5A0787C +:10AD2000002829D162692046D37801690D2B01F1F1 +:10AD300070005FD00DDCA3F102034FF001050B2B77 +:10AD400019D2DFE803F01A1844506127182C183A7A +:10AD50006400152B6FD008DC112B4BD0122B5AD06E +:10AD6000132B62D0142B06D166E0162B71D0172B53 +:10AD700070D0FF2B6FD0FFDF70BD91F87F200123D3 +:10AD80001946F6F7FFF80028F6D12169082081F866 +:10AD90007F0070BD1079BDE8704001F090BC91F863 +:10ADA0007E00C00700D1FFDF01F048FC206910F8E9 +:10ADB0007E1F21F00101017070BD91F87D00102807 +:10ADC00000D0FFDF2069112180F8A75008E091F83A +:10ADD0007D00142800D0FFDF2069152180F8A750DE +:10ADE00080F87D1070BD91F87D00152800D0FFDF40 +:10ADF000172005E091F87D00152800D0FFDF19200D +:10AE0000216981F87D0070BDBDE870404EE7BDE866 +:10AE1000704001F028BC91F87C2001230021F6F756 +:10AE2000B1F800B9FFDF0E200FE011F87E0F20F01F +:10AE3000040008701DE00FE091F87C200123002140 +:10AE4000F6F7A0F800B9FFDF1C20216981F87C002B +:10AE500070BD12E01BE022E091F87E00C0F301100B +:10AE6000012800D0FFDF206910F87E1F21F01001BB +:10AE70000170BDE8704001F0E1BB91F87C20012336 +:10AE80000021F6F77FF800B9FFDF1F20DDE791F81A +:10AE90007D00212801D000B1FFDF2220B0E7BDE80E +:10AEA000704001F0D7BB2F48016991F87E2013074D +:10AEB00002D501218170704742F0080281F87E209E +:10AEC0008069C07881F8E10001F0AFBB10B5254C76 +:10AED00021690A88A1F8162281F8140291F8640009 +:10AEE00001F091FB216981F8180291F8650001F0E9 +:10AEF0008AFB216981F81902012081F812020020E1 +:10AF000081F8C0012079BDE81040E7F7FDBF10B51A +:10AF1000144C05212069FFF763FC206990F85A1052 +:10AF2000012908D000F5F57103F001FC2079BDE896 +:10AF30001040E7F7E9BF022180F85A1010BD10B5A4 +:10AF4000084C01230921206990F87C207030F6F725 +:10AF500019F848B12169002001F8960F087301F82B +:10AF60001A0C10BD000100200120A070F9E770B597 +:10AF7000F74D012329462869896990F87C200979D1 +:10AF80000E2A01D1122903D000241C2A03D004E088 +:10AF9000BDE87040D3E7142902D0202A07D008E08A +:10AFA00080F87C4080F8A240BDE87040AFE71629E9 +:10AFB00006D0262A01D1162902D0172909D00CE083 +:10AFC00000F87C4F80F82640407821280CD01A20C9 +:10AFD00017E090F87D20222A07D0EA69002A03D0E2 +:10AFE000FF2901D180F8A23132E780F87D4001F0DD +:10AFF00025FB286980F8974090F8C0010028F3D01D +:10B000000020BDE8704061E710B5D14C216991F88E +:10B010007C10202902D0262902D0A2E7FFF756FF94 +:10B020002169002081F87C0081F8A20099E72DE9D0 +:10B03000F843C74C206990F87C10202908D00027DD +:10B0400090F87D10222905D07FB300F17C0503E044 +:10B050000127F5E700F17D0510F8B01F41F004016C +:10B060000170A06903F015FA4FF00108002608B33B +:10B070003946A069FFF771FDE0B16A46A169206910 +:10B08000F6F7F7F890B3A06903F001FA2169A1F887 +:10B09000AA01B1F8701001F0AAFA40B32069282182 +:10B0A00080F88D1080F88C8058E0FFE70220A070B7 +:10B0B000BDE8F883206990F8C00110B11E20FFF7A9 +:10B0C00005FFAFB1A0692169C07881F8E20008FAF4 +:10B0D00000F1C1F3006000B9FFDF20690A2180F8A8 +:10B0E0007C1090F8A20040B9FFDF06E009E02AE0FA +:10B0F0002E7001F0A3FAFFF7D6FE206980F8976062 +:10B10000D6E7226992F8C00170B1B2F8703092F8B7 +:10B110006410B2F8C40102F5D572F6F79BF968B174 +:10B120002169252081F87C00206900F17D0180F8EB +:10B1300097608D4212D180F87D600FE00020FFF70C +:10B14000C5FE2E70F0E720699DF8001080F8AC1164 +:10B150009DF8011080F8AD1124202870206900F1BD +:10B160007D018D4203D1BDE8F84301F067BA80F854 +:10B17000A2609DE770B5764C01230B21206990F801 +:10B180007D207030F5F7FEFE202650BB206901239C +:10B19000002190F87D207030F5F7F4FE0125F0B124 +:10B1A000206990F87C0024281BD0A06903F04FF997 +:10B1B000C8B1206990F8B01041F0040180F8B010D7 +:10B1C000A1694A7902F0070280F85D20097901F04F +:10B1D000070180F85C1090F8C1311BBB06E0A57038 +:10B1E00036E6A67034E6BDE870405CE690F8C03103 +:10B1F000C3B900F164035E788E4205D1197891429B +:10B2000002D180F897500DE000F503710D700288AF +:10B210004A8090F85C200A7190F85D0048712079AE +:10B22000E7F772FE2169212081F87D00BDE87040BA +:10B2300001F0FBB9F8B5464C206990F87E0010F09B +:10B24000300F04D0A07840F00100A070F8BDA069D4 +:10B2500003F0E2F850B3A06903F0D8F80746A069FC +:10B2600003F0D8F80646A06903F0CEF80546A069B9 +:10B2700003F0CEF801460097206933462A46303065 +:10B2800003F0BFF9A079800703D56069C07814285E +:10B290000FD0216991F87C001C280AD091F85A003F +:10B2A00001280ED091F8B70158B907E0BDE8F84081 +:10B2B000F9E52169012081F85A0002E091F8B60110 +:10B2C00030B1206910F87E1F41F0100101700EE0CE +:10B2D00091F87E0001F5FC7240F0200081F87E00BC +:10B2E00031F8300B03F017FA2079E7F70DFEBDE8CF +:10B2F000F84001F09AB970B5154C206990F87E10AD +:10B30000890707D590F87C20012308217030F5F7D4 +:10B3100039FEF8B1206990F8AA00800712D4A0691C +:10B3200003F056F8216981F8AB00A06930F8052FC9 +:10B33000A1F8AC204088A1F8AE0011F8AA0F40F0A7 +:10B3400002000870206990F8AA10C90705D011E022 +:10B35000000100200120A0707AE590F87E008007AF +:10B3600000D5FFDF206910F87E1F41F00201017057 +:10B3700001F05BF92069002590F87C10062906D1C0 +:10B3800080F87C5080F8A2502079E7F7BDFD206955 +:10B3900090F8A8110429DFD180F8A8512079E7F7A7 +:10B3A000B3FD206990F87C100029D5D180F8A25017 +:10B3B0004EE570B5FB4C01230021206990F87D20FB +:10B3C0007030F5F7DFFD012578B9206990F87D2010 +:10B3D000122A0AD0012305217030F5F7D3FD10B1F0 +:10B3E0000820A07034E5A57032E5206990F8A80027 +:10B3F00008B901F01AF92169A06901F5847102F018 +:10B40000C8FF2169A069D83102F0CEFF206990F809 +:10B41000DC0100B1FFDF21690888A1F8DE0101F538 +:10B42000F071A06902F0A3FF2169A06901F5F47130 +:10B4300002F0A5FF206980F8DC51142180F87D100E +:10B440002079BDE87040E7F75FBD70B5D54C0123AA +:10B450000021206990F87D207030F5F793FD0125DB +:10B46000A8B1A06902F04FFF98B1A0692169B0F8B6 +:10B470000D00A1F8AA01B1F8701001F0B8F858B1A8 +:10B480002069282180F88D1080F88C50E0E4A570A8 +:10B49000DEE4BDE8704006E5A0692169027981F823 +:10B4A000AC21B0F80520A1F8AE2102F01FFF216900 +:10B4B000A1F8B001A06902F01CFF2169A1F8B20156 +:10B4C000A06902F01DFF2169A1F8B4010D2081F8E7 +:10B4D0007D00BDE47CB5B34CA079C00738D0A0692D +:10B4E00001230521C578206990F87D207030F5F79B +:10B4F00049FD68B1AD1E0A2D06D2DFE805F0090945 +:10B500000505090905050909A07840F00800A070A3 +:10B51000A07800281CD1A06902F0BEFE00286ED0E1 +:10B52000A0690226C5781DB1012D01D0162D18D1B4 +:10B53000206990F87C00F5F70DFD90B1216991F834 +:10B540007C001F280DD0202803D0162D16D0A67001 +:10B550007CBD262081F87C00162D02D02A20FFF722 +:10B56000B5FC0C2D5BD00CDC0C2D48D2DFE805F0CF +:10B5700036331F48BEBE4BB55ABE393C2020A070A2 +:10B580007CBD0120142D6ED008DC0D2D6CD0112D4A +:10B590006BD0122D6ED0132D31D168E0152D7FD0D8 +:10B5A000162D6FD0182D6ED0FF2D28D198E0206970 +:10B5B0000123194690F87F207030F5F7E3FC00284E +:10B5C00008D1A06902F0CCFE216981F88E01072024 +:10B5D00081F87F008CE001F0EDF889E0FFF735FF9E +:10B5E00086E001F0C7F883E0206990F87D1011290A +:10B5F00001D0A6707CE0122180F87D1078E075E023 +:10B60000FFF7D7FE74E0206990F87D001728F0D18D +:10B6100001F014F821691B2081F87D0068E0FFF734 +:10B620006AFE65E0206990F87E00C00703D0A0782C +:10B6300040F0010023E06946A06902F0D0FE9DF8C9 +:10B64000000000F02501206900F8B01F9DF80110EE +:10B6500001F04901417000F0E8FF206910F87E1FF9 +:10B6600041F0010117E018E023E025E002E0FFF7D8 +:10B6700066FC3DE0216991F87E10490704D5A07071 +:10B6800036E00DE00FE011E000F0CFFF206910F888 +:10B690007E1F41F0040101702AE0FFF7CBFD27E097 +:10B6A00001F030F824E0FFF765FD21E0FFF7BFFC73 +:10B6B0001EE0A06900790DE0206910F8B01F41F08C +:10B6C00004010170A06902F0F7FE162810D1A069EC +:10B6D00002F0F6FEFFF798FC0AE0FFF748FC07E0EF +:10B6E000E16919B1216981F8A20101E0FFF7DBFBF3 +:10B6F0002169F1E93002401C42F10002C1E9000277 +:10B700007CBD70B5274CA07900074AD5A0780028E9 +:10B7100047D1206990F8E400FE2800D1FFDF2069BE +:10B72000FE21002580F8E41090F87D10192906D13B +:10B7300080F8A75000F082FF206980F87D502069D2 +:10B7400090F87C101F2902D0272921D119E090F808 +:10B750007D00F5F7FFFB78B120692621012380F8F1 +:10B760007C1090F87D200B217030F5F70BFC78B938 +:10B770002A20FFF7ABFB0BE02169202081F87C0039 +:10B7800006E0012180F8A11180F87C5080F8A250D9 +:10B79000206990F87F10082903D10221217080F8D8 +:10B7A000E41021E40001002010B5FD4C216991F85E +:10B7B000AC210AB991F8642081F8642091F8AD2198 +:10B7C0000AB991F8652081F8652010B10020FFF7D3 +:10B7D0007DFB206902F041FF002806D02069BDE80A +:10B7E000104000F5F57102F0A2BF16E470B5EC4C04 +:10B7F00006460D46206990F8E400FE2800D0FFDFE1 +:10B800002269002082F8E46015B1A2F8A400E7E400 +:10B8100022F89E0F01201071E2E470B5E04C012384 +:10B820000021206990F87C207030F5F7ABFB0028F0 +:10B830007BD0206990F8B61111B190F8B71139B1E9 +:10B8400090F8C01100296FD090F8C11119B36BE0C6 +:10B8500090F87D1024291CD090F87C10242918D051 +:10B860005FF0000300F5D67200F5DB7102F096FE82 +:10B870002169002081F8B60101461420FFF7B6FFC8 +:10B88000216901F13000C28A21F8E62F408B4880FF +:10B8900050E00123E6E790F87D2001230B21703072 +:10B8A000F5F770FB68BB206990F8640000F0ABFE10 +:10B8B0000646206990F8650000F0A5FE054620695F +:10B8C00090F8C2113046FFF735F9D8B1206990F8E9 +:10B8D000C3112846FFF72EF9A0B12269B2F87030E3 +:10B8E00092F86410B2F8C40102F5D572F5F7B2FD12 +:10B8F00020B12169252081F87C001BE00020FFF7A2 +:10B90000E5FA11E020690123032190F87D207030D1 +:10B91000F5F738FB40B920690123022190F87D201A +:10B920007030F5F72FFB08B1002059E400211620F4 +:10B93000FFF75CFF012053E410B5E8BB984C206989 +:10B9400090F87E10CA0702D00121092052E08A0730 +:10B950000AD501210C20FFF749FF206910F8AA1F22 +:10B9600041F00101017047E04A0702D5012113208F +:10B9700040E00A0705D510F8E11F417101210720B9 +:10B9800038E011F0300F3BD090F8B711A1B990F822 +:10B99000B611E1B190F87D1024292FD090F87C10D9 +:10B9A00024292BD05FF0000300F5D67200F5DB717F +:10B9B00002F0F4FD216900E022E011F87E0F20F092 +:10B9C000200040F010000870002081F83801206944 +:10B9D00090F87E10C90613D502F03FFEFFF797FAE4 +:10B9E000216901F13000C28A21F8E62F408B48809E +:10B9F00001211520FFF7FAFE0120F6E60123D3E727 +:10BA00000020F2E670B5664C206990F8E410FE293B +:10BA100078D1A178002975D190F87F2001231946AB +:10BA20007030F5F7AFFA00286CD1206990F88C11CE +:10BA300049B10021A0F89C1090F88D1180F8E61013 +:10BA4000002102205BE090F87D200123042170306A +:10BA5000F5F798FA0546FFF76FFF002852D1284600 +:10BA600000F00CFF00284DD120690123002190F83F +:10BA70007C207030F5F786FA78B120690123042123 +:10BA800090F87D207030F5F77DFA30B9206990F894 +:10BA9000960010B10021122031E0206990F87C203E +:10BAA0000A2A0DD0002D2DD1012300217030F5F789 +:10BAB00069FA78B1206990F8A81104290AD105E043 +:10BAC00010F8E21F01710021072018E090F8AA0089 +:10BAD000800718D0FFF7A1FE002813D120690123A9 +:10BAE000002190F87C207030F5F74CFA002809D03E +:10BAF000206990F8A001002804D00021FF20BDE8B3 +:10BB0000704073E609E000210C20FFF76FFE20690A +:10BB100010F8AA1F41F0010101701DE43EB5054671 +:10BB20006846FDF7ABFC00B9FFDF22220021009838 +:10BB3000F2F7ADFC0321009802F096FB0098017823 +:10BB400021F010010170294602F0B3FB144C0D2DB9 +:10BB500043D00BDCA5F102050B2D19D2DFE805F06F +:10BB600022184B191922185718192700152D5FD0C4 +:10BB700008DC112D28D0122D0BD0132D09D0142D37 +:10BB800006D155E0162D2CD0172D6AD0FF2D74D07C +:10BB9000FFDFFDF786FC002800D1FFDF3EBD00007F +:10BBA000000100202169009891F8E61017E0E26892 +:10BBB00000981178017191884171090A8171518849 +:10BBC000C171090A0172E4E70321009802F072FCD6 +:10BBD0000621009802F072FCDBE700980621017153 +:10BBE000D7E70098D4F8101091F8C221027191F8AB +:10BBF000C3114171CDE72169009801F5887102F008 +:10BC0000D7FB21690098DC3102F0DCFBC1E7FA497F +:10BC1000D1E90001CDE90101206901A990F8B00046 +:10BC200000F025008DF80400009802F006FCB0E753 +:10BC30002069B0F84810009802F0D6FB2069B0F8EF +:10BC4000E810009802F0D4FB2069B0F84410009886 +:10BC500002F0D2FB2069B0F8E610009802F0D0FBA9 +:10BC600097E7216991F8C00100280098BCD111F82C +:10BC7000642F02714978BCE7FFE7206990F8A3219F +:10BC8000D0F8A411009802F022FB82E7DB4810B53F +:10BC9000006990F8821041B990F87D2001230621B7 +:10BCA0007030F5F76FF9002800D001209DE570B5E0 +:10BCB000D24D286990F8801039B1012905D00229A8 +:10BCC00006D0032904D0FFDF03E4B0F8F41037E016 +:10BCD00090F87F10082936D0B0F89810B0F89A2064 +:10BCE00000248B1C9A4206D3511A891E0C04240C82 +:10BCF00001D0641EA4B290F8961039B190F87C205F +:10BD0000012309217030F5F73DF940B3FFF7BEFF7D +:10BD100078B129690020B1F89020B1F88E108B1C01 +:10BD20009A4203D3501A801E00D0401EA04200D277 +:10BD300084B20CB1641EA4B22869B0F8F410214496 +:10BD4000A0F8F0102DE5B0F898100329BDD330F815 +:10BD5000701F428D1144491CA0F8801021E5002479 +:10BD6000EAE770B50C4605464FF4087200212046FC +:10BD7000F2F78DFB258014E5F8F7A2B92DE9F04123 +:10BD80000D4607460721F8F791F8041E3CD094F8B9 +:10BD9000C8010026A8B16E70092028700BE0268427 +:10BDA00084F8C861D4F8CA016860D4F8CE01A860EC +:10BDB000B4F8D201A88194F8C8010028EFD12E71FF +:10BDC000AEE094F8D40190B394F8D4010D2813D0C8 +:10BDD0000E2801D0FFDFA3E02088F8F798F9074686 +:10BDE000F7F745FE78B96E700E20287094F8D601EA +:10BDF00028712088E88014E02088F8F788F9074641 +:10BE0000F7F735FE10B10020BDE8F0816E700D200F +:10BE1000287094F8D60128712088E88094F8DA0117 +:10BE2000287284F8D4613846F7F71BFE78E0FFE704 +:10BE300094F80A0230B16E701020287084F80A62FB +:10BE4000AF806DE094F8DC0190B16E700A2028702C +:10BE50002088A880D4F8E011C5F80610D4F8E411C1 +:10BE6000C5F80A10B4F8E801E88184F8DC6157E00D +:10BE700094F8040270B16E701A20287005E000BFBB +:10BE800084F80462D4F80602686094F8040200287A +:10BE9000F6D145E094F8EA0188B16E70152028705B +:10BEA00008E000BF84F8EA6104F5F6702B1D07C8AE +:10BEB00083E8070094F8EA010028F3D130E094F811 +:10BEC000F80170B16E701C20287084F8F861D4F805 +:10BED000FA016860D4F8FE01A860B4F80202A881F3 +:10BEE0001EE094F80C0238B11D20287084F80C6212 +:10BEF000D4F80E02686013E094F81202002883D090 +:10BF00006E701620287007E084F81262D4F81402CC +:10BF10006860B4F81802288194F812020028F3D15E +:10BF2000012071E735480021C16101620846704770 +:10BF300030B5324D0C46E860FFF7F4FF00B1FFDF8B +:10BF40002C7130BD002180F87C1080F87D1080F8C5 +:10BF5000801090F8FB1009B1022100E00321FEF7E8 +:10BF60003FBC2DE9F041254C0546206909B100216F +:10BF700004E0B0F80611B0F8F6201144A0F806115C +:10BF800090F88C1139B990F87F2001231946703050 +:10BF9000F4F7F8FF30B1206930F89C1FB0F85A2050 +:10BFA00011440180206990F8A23033B1B0F89E109E +:10BFB000B0F8F6201144A0F89E1090F9A670002F5A +:10BFC00006DDB0F8A410B0F8F6201144A0F8A410D3 +:10BFD00001213D2615B180F88D6017E02278022AF4 +:10BFE0000ED0012A15D0A2784AB380F88C1012F036 +:10BFF000140F11D01E2117E0FC6202000001002086 +:10C0000090F8E620062A3CD016223AE080F88C1000 +:10C0100044E090F88E2134E0110702D580F88D605D +:10C020003CE0910603D5232180F88D1036E090077F +:10C0300000D1FFDF21692A2081F88D002AE02BB191 +:10C04000B0F89E20B0F8A0309A4210D2002F05DD43 +:10C05000B0F8A420B0F8A0309A4208D2B0F89C30D2 +:10C06000B0F89A20934204D390F88C310BB122227D +:10C0700007E090F880303BB1B0F89830934209D394 +:10C08000082280F88D20C1E7B0F89820062A01D355 +:10C090003E22F6E7206990F88C1019B12069BDE8BE +:10C0A000F0414FE7BDE8F0410021FEF799BB2DE9D3 +:10C0B000F047FF4C81460D4620690088F8F739F8B3 +:10C0C000060000D1FFDFA0782843A070A0794FF0D0 +:10C0D00000058006206904D5A0F8985080F8045126 +:10C0E00003E030F8981F491C0180FFF7CFFD4FF0A7 +:10C0F000010830B3E088000506D5206990F8821069 +:10C1000011B1A0F88E501CE02069B0F88E10491CC7 +:10C1100089B2A0F88E10B0F890208A4201D3531A49 +:10C1200000E0002327897F1DBB4201D880F896805C +:10C13000914206D3A0F88E5080F80A822079E6F763 +:10C14000E3FEA0794FF0020710F0600F0ED02069D7 +:10C1500090F8801011B1032908D102E080F88080A6 +:10C1600001E080F880700121FEF73AFB206990F829 +:10C170008010012904D1E188C90501D580F88070BB +:10C18000B9F1000F72D1E188890502D5A0F81851E4 +:10C1900004E0B0F81811491CA0F8181100F035FBA4 +:10C1A000FEF719FDFFF72EFC2769B7F8F800401CD1 +:10C1B000A7F8F80097F8FC0028B100F01BFFA8B121 +:10C1C000A7F8F85012E000F012FF08B1A7F8F850F5 +:10C1D00000F015FF50B197F80401401CC0B287F879 +:10C1E0000401022802D927F8F85F3D732069012372 +:10C1F000002190F87D207030F4F7C4FE20B920694A +:10C2000090F87D000C2859D120690123002190F875 +:10C210007C207030F4F7B6FE48B32069012300217A +:10C2200090F87F207030F4F7ADFE00B3206990F8ED +:10C230008010022942D190F80401C0B93046F7F7C6 +:10C24000E6F9A0B1216991F8E400FE2836D1B1F8F1 +:10C25000F200012832D981F8FA80B1F89A00B1F8D9 +:10C260009820831E9A4203DB012004E032E025E09F +:10C27000801A401E80B2B1F8F82023899A4201D377 +:10C28000012202E09A1A521C92B2904200D9104642 +:10C29000012801D181F8FA5091F86F2092B98A6E85 +:10C2A00082B1B1F89420B1F87010511A09B2002986 +:10C2B00008DD884200DB084680B203E021690120E6 +:10C2C00081F8FA502169B1F870201044A1F8F40007 +:10C2D000FFF7EDFCE088C0F340214846FFF741FE40 +:10C2E000206980F8FB50BDE8F047FDF7FCB87049C5 +:10C2F00002468878CB78184312D10846006942B1CB +:10C300008979090703D590F87F00082808D0012013 +:10C310007047B0F84C10028E914201D8FEF7B1B9C7 +:10C320000020704770B5624C05460E46E0882843F1 +:10C33000E080A80703D5E80700D0FFDF6661EA07C1 +:10C340004FF000014FF001001AD0A661F278062AE2 +:10C3500002D00B2A14D10AE0226992F87D30172B03 +:10C360000ED10023E2E92E3302F8370C08E02269EF +:10C3700092F87D30112B03D182F8811082F8A80049 +:10C38000AA0718D56269D278052A02D00B2A12D1E1 +:10C390000AE0216991F87D20152A0CD10022E1E9FB +:10C3A000302201F83E0C06E0206990F87D20102A2A +:10C3B00001D180F88210280601D50820E07083E4BE +:10C3C0002DE9F84301273A4C002567F30701E58082 +:10C3D000A570E570257020618946804680F8FB7065 +:10C3E0000088F7F7A6FE00B9FFDF20690088FDF797 +:10C3F00042F820690088FDF764F82069B0F8F2106F +:10C4000071B190F8E410FE290FD190F88C1189B128 +:10C4100090F87F20012319467030F4F7B3FD78B10E +:10C42000206990F8E400FE2804D0206990F8E40028 +:10C43000FFF774FB206990F8FD1089B1258118E0A1 +:10C440002069A0F89C5090F88D1180F8E61000212A +:10C450000220FFF7CBF9206980F8FA500220E7E7C5 +:10C4600090F8C81119B9018C8288914200D881884E +:10C47000218130F8F61F491E8EB230F8021F314478 +:10C4800020F86019018831440180FFF7FFFB20B1DB +:10C49000206930F88E1F314401802069B0F8F21015 +:10C4A000012902D8491CA0F8F2102EB102E00000C8 +:10C4B0000001002080F8045180F8FA5090F87D10B7 +:10C4C0000B2901D00C2916D1B0F87020B0F8AA3190 +:10C4D000D21A12B2002A0EDBD0F8AC11816090F8AB +:10C4E000B01101730321F4F773F8206980F87D50CF +:10C4F00080F8B27026E0242910D1B0F87010B0F89E +:10C50000AA21891A09B2002908DB90F8C001FFF7B7 +:10C510004BF9206900F87D5F857613E090F87C1078 +:10C52000242901D025290DD1B0F87010B0F8AA0146 +:10C53000081A00B2002805DB0120FFF735F9206951 +:10C5400080F87C5020690146B0F8F6207030F4F78E +:10C55000B2FAFC480090FC4BFC4A4146484600F0C9 +:10C560007DFC216A11B16078FCF7B5FA20690123DE +:10C57000052190F87D207030F4F704FD002803D0E9 +:10C58000BDE8F84300F0FDB9BDE8F88300F015BD43 +:10C59000EF49C8617047EE48C069002800D001200B +:10C5A0007047EB4A50701162704710B5044600881E +:10C5B000A4F8CC01B4F8B001A4F8CE01B4F8B201EB +:10C5C000A4F8D001B4F8B401A4F8D201012084F891 +:10C5D000C801DF480079E6F797FC02212046F3F70F +:10C5E000F7FF002004F87D0F0320E07010BD401A13 +:10C5F00000B247F6FE71884201DC002801DC012010 +:10C6000070470020704710B5012808D0022808D0D4 +:10C61000042808D0082806D0FFDF204610BD0124DA +:10C62000FBE70224F9E70324F7E7C9480021006982 +:10C6300020F8A41F8178491C81707047C44800B558 +:10C64000016911F8A60F401E40B20870002800DAF8 +:10C65000FFDF00BDBE482721006980F87C10002163 +:10C6600080F8A011704710B5B94C206990F8A81156 +:10C67000042916D190F87C20012300217030F4F7B2 +:10C6800081FC00B9FFDF206990F8AA10890703D464 +:10C69000062180F87C1004E0002180F8A21080F8C8 +:10C6A000A811206990F87E00800707D5FFF7C6FF24 +:10C6B000206910F87E1F21F00201017010BDA4490D +:10C6C00010B5096991F87C200A2A09D191F8E22075 +:10C6D000824205D1002081F87C0081F8A20010BDC3 +:10C6E00091F87E20130706D522F0080081F87E001D +:10C6F000BDE81040A2E7FF2801D0FFDF10BDBDE874 +:10C700001040A7E7F8B5924C01230A21206990F860 +:10C710007C207030F4F736FC38B3A06901F07CFE61 +:10C72000C8B1A06901F072FE0746A06901F072FE6F +:10C730000646A06901F068FE0546A06901F068FEA2 +:10C7400001460097206933462A46303001F059FFF0 +:10C75000206901F082FF2169002081F8A20081F8A0 +:10C760007C00BDE8F840FEF7D2BBA07840F00100A5 +:10C77000A070F8BD10B5764C01230021206990F817 +:10C780007D207030F4F7FEFB30B1FFF74EFF2169DA +:10C79000102081F87D0010BD20690123052190F84B +:10C7A0007D207030F4F7EEFB08B1082000E0012096 +:10C7B000A07010BD70B5664C01230021206990F86F +:10C7C0007D207030F4F7DEFB012588B1A06901F00F +:10C7D000C4FD2169A1F8AA01B1F87010FFF707FFA5 +:10C7E00040B12069282180F88D1080F88C50E6E552 +:10C7F000A570E4E52169A06901F5D67101F0A8FDF5 +:10C8000021690B2081F87D00D9E510B5FEF779FF8D +:10C81000FEF760FE4E4CA079400708D5A07830B9ED +:10C82000206990F87F00072801D101202070FEF7D1 +:10C8300071FAA079C00609D5A07838B9206990F8B6 +:10C840007D100B2902D10C2180F87D10E0780007C3 +:10C850000ED520690123052190F87D207030F4F772 +:10C8600091FB30B10820A0702169002081F8D4012B +:10C8700010BDBDE81040002000F0C4BB10B5344C22 +:10C88000216991F87D2048B3102A06D0142A07D0D8 +:10C89000152A1AD01B2A2CD11AE001210B2019E0ED +:10C8A000FAF702FE0C2817D32069082100F58870DA +:10C8B000FAF7FEFD28B120690421DC30FAF7F8FD13 +:10C8C00000B9FFDF0121042004E000F017F803E0C5 +:10C8D00001210620FEF78AFF012010BD212A08D180 +:10C8E00091F8970038B991F8C00110B191F8C101E1 +:10C8F00008B1002010BD01211720EBE770B5144CE2 +:10C900000025206990F88F1101290AD002292ED123 +:10C9100090F8A810F1B1062180F8E610012102205C +:10C9200020E090F8D411002921D100F1C80300F5CE +:10C930008471002200F5C870F4F796FA01210520F1 +:10C9400010E00000AFC00100EFC2010025C30100EC +:10C950000001002090F8B000400701D5112000E050 +:10C960000D200121FEF742FF206980F88F5126E556 +:10C9700030B5FB4C05462078002818BFFFDFE57175 +:10C9800030BDF7490120887170472DE9F14FF54D11 +:10C990002846446804F1700794F86510608F94F895 +:10C9A0008280268F082978D0F4F797FBB8F1000F22 +:10C9B00004BF001D80B2864238BF304600F0FF0839 +:10C9C000DFF89C93E848C9F8240009F134006E6848 +:10C9D000406800F1700A90F882B096F86510358FC3 +:10C9E000708F08295DD0F4F778FB00BFBBF1000F12 +:10C9F00004BF001D80B2854238BF2846C0B29AF8F5 +:10CA00001210002918BF04210844C0B296F865101E +:10CA1000FBF735FCB87C002847D007F15801D24815 +:10CA200091E80E1000F5027585E80E10B96EC0F899 +:10CA30002112F96EC0F8251200F58170FBF7DBFFBB +:10CA4000C848007800280CBF0120002080F00101B8 +:10CA5000C6480176D7E91412C0E90412A0F5837222 +:10CA6000D9F82410FBF7F5F994F86500012808BF00 +:10CA700000220CD0022808BF012208D0042808BFD9 +:10CA8000032204D008281ABFFFDF002202224146F9 +:10CA90000120FBF7F9F90EE0FFE70421F4F71DFB95 +:10CAA00084E70421F4F719FBA0E7D9F82400FBF789 +:10CAB000A2FFFBF715FA009850B994F8650094F8B6 +:10CAC000661010F00C0F08BF00219620FBF7B4FF92 +:10CAD00094F8642001210020FCF76BF894F82C00F6 +:10CAE000012808BFFCF735F8022089F80000FCF7A0 +:10CAF0003FFC002818BFFFDFBDE8F88F2DE9F04F9D +:10CB0000DFF860A28BB050469AF800204068AAF186 +:10CB10001401059190F8751000F1700504464FF06E +:10CB200008080127AAF13406A1B3012900F0068103 +:10CB3000022900F00781032918BFFFDF00F01881E8 +:10CB4000306A0423017821F008010170AA7908EA0B +:10CB5000C202114321F004010170EA7903EA820262 +:10CB6000114321F01001017095F80590F06AF6F775 +:10CB70005EFD8046FCF7C9FCB9F1020F00F00081B0 +:10CB8000B9F1010F00F00081B9F1030F00F000814D +:10CB900000F003B9FFE795F80CC04FF002094FF021 +:10CBA000000BBCF1240F1CBF6B7B242B08D0BCF105 +:10CBB0001F0F18BFBCF1200F2AD0222B4DD077E0D9 +:10CBC00094F864109AB190F8AC01002874D0082948 +:10CBD00018BF042969D0082818BF042865D0012986 +:10CBE00018BF012853D000BF4FF0020164E090F855 +:10CBF0001201002860D0082918BF042955D0082840 +:10CC000018BF042851D0012918BF01283FD0EBE7F5 +:10CC1000222B22D0002A4BD090F8C20194F8641045 +:10CC200010F0040F18BF40460CD0082918BF042983 +:10CC30003BD0082818BF042837D0012918BF012885 +:10CC400025D0D1E710F0010F18BF3846EDD110F014 +:10CC5000020F18BF4846E8D12EE04AB390F8C2212F +:10CC600090F85D0094F8641002EA000010F0040FE0 +:10CC700018BF40460ED0082918BF042915D008282F +:10CC800018BF042811D0012918BF0128ACD14FF0DA +:10CC9000010111E010F0010F18BF3846EBD110F080 +:10CCA000020F18BF4846E6D106E04FF0080103E046 +:10CCB00094F864100429F8D0A08E11F00C0F18BF5E +:10CCC0004FF42960F4F709FA218E814238BF0846F3 +:10CCD000ADF80400A4F84C000598FCF7F5FB60B132 +:10CCE0007289316A42F48062728172694FF48060A5 +:10CCF000904703206871EF7022E709AA01A9F06A42 +:10CD0000F6F7CFFB306210B195F8371021B10598D6 +:10CD1000FCF7AEFB6F7113E79DF8241031B9A0F852 +:10CD200000B080F802B0012101F09EFABDF80410B5 +:10CD3000306A01F0C7FB85F8059001E70598FCF71C +:10CD400097FBFDE6B4F84C00ADF8040009AA01A970 +:10CD5000F06AF6F7A6FB3062002808BFFFDFEFE6B7 +:10CD60002401002058010020300D0020380F002041 +:10CD70000598FCF7A9FB002808BFFFDFE0E600BF2D +:10CD800030EA080009D106E030EA080005D102E0E7 +:10CD9000B8F1000F01D0012100E00021306A0278D3 +:10CDA00042EA01110170697C00291CBF69790129DF +:10CDB0003BD005F15801FD4891E80E1000F50278CE +:10CDC00088E80E10A96EC0F82112E96EC0F825128D +:10CDD00000F58170FBF70FFE9AF8000000280CBFE9 +:10CDE00001210021F2480176D5E91212C0E90412AE +:10CDF000A0F58371326AFBF72CF894F864000128DF +:10CE000008BF00220CD0022808BF012208D0042845 +:10CE100008BF032204D008281ABFFFDF0022022225 +:10CE2000FB210020FBF730F803E0FBF7E4FDFBF704 +:10CE300057F8012194F865200846FBF7BAFE3771D0 +:10CE4000306A0188F181807830743770FCF799FA84 +:10CE5000002818BFFFDF0BB0BDE8F08F2DE9F043CD +:10CE6000D44D87B081462878DDF838801E461746B5 +:10CE70000C4628B9002F1CBF002EB8F1000F00D1BE +:10CE8000FFDFC5F81C80C5E90D94C5E905764FF0B4 +:10CE90000000A8716871E870A8702871C64E68819A +:10CEA000A881307804F170072088F7F742F9E8622A +:10CEB0002088F7F72CF92863FBF705FA94F9670047 +:10CEC000FBF7DAFA04F11200FBF76CFD04F10E0037 +:10CED000FBF7D8FA307800280CBF03200120FBF7BD +:10CEE00087FDB64890E80E108DE80E10D0E90410CA +:10CEF000CDE90410307800280CBFB148B148049047 +:10CF00006846FBF763FDF87EFBF7C4FAFBF76AFDA2 +:10CF100094F86F0078B9A06E68B1B88C39888842EF +:10CF200009D1B4F86C1001220844B88494F86E005A +:10CF3000A16EF8F7D8FE3078002804BFFF2094F8DF +:10CF400064401AD094F8651097F81280258F608F8E +:10CF5000082926D0F4F7C1F8B8F1000F04BF001D6E +:10CF600080B2854238BF2846C0B2B97C002918BFBC +:10CF70000421084494F86540C0B22146FBF77FF9CC +:10CF80003078214688B10120FBF74BFB7068D0F860 +:10CF90000001FBF733FD0120FFF7F7FC07B0BDE808 +:10CFA000F0830421F4F799F8D6E70020FBF739FB6A +:10CFB000FFF7A4FD07B0BDE8F0837F4800B5017816 +:10CFC0003438007819B1022818BFFFDF00BD0128EE +:10CFD00018BFFFDF00BD774810B50078022818BFE2 +:10CFE000FFDFBDE8104000F070BA00F06EBA714883 +:10CFF000007970476F488089C0F3002070476D4802 +:10D00000C07870472DE9F04706006B48694D4068CD +:10D0100000F17004686A90F8019018BF012E03D1E6 +:10D02000296B07F0F1FF6870687800274FF001085E +:10D03000A0B101283CD0022860D003281CBFFFDF2C +:10D04000BDE8F087012E08BFBDE8F087286BF6F732 +:10D05000E3FCE879BDE8F047E5F756BF012E14D0B0 +:10D06000A86A002808BFFFDF2889C21CD5E909107B +:10D07000F1F7E3F9A86A686201224946286BF6F7DE +:10D0800047FB022E08BFBDE8F087D4E91401401C1D +:10D0900041F10001C4E91401E079012801D1E771EF +:10D0A00001E084F80780E879BDE8F047E5F72CBF98 +:10D0B000012E14D0A86A002808BFFFDF2889C21CEF +:10D0C000D5E90910F1F7B9F9A86A68620022494662 +:10D0D000286BF6F71DFB022E08BFBDE8F087D4E9E8 +:10D0E0001410491C40F10000C4E91410E079012833 +:10D0F0000CBFE77184F80780BDE8F087012E06D0E9 +:10D10000286BF6F789FC022E08BFBDE8F087D4E94A +:10D110001410491C40F10000C4E91410E079012802 +:10D12000BFD1BCE72DE9F041234D2846A5F13404D9 +:10D13000406800F170062078012818BFFFDFB07842 +:10D140000127002158B1B1706289042042F0040225 +:10D150006281626990472878002818BF3771216A78 +:10D160000322087832EA000009D1628912F4806F44 +:10D1700005D042F002026281626902209047A169F3 +:10D180000020884760B3607950BB287818B30E48F8 +:10D19000007810F0100F04D10449097811F0100F35 +:10D1A0001ED06189E1B9A16AA9B90FE0300D002054 +:10D1B000380F0020240100205801002004630200E1 +:10D1C000BB220200A7A8010032010020218911B171 +:10D1D00010F0100F04D0BDE8F0410020FFF7D5BBE0 +:10D1E000BDE8F04100F071B92DE9F05FCC4E044686 +:10D1F0003046A6F134054068002700F1700A28780F +:10D20000B846022818BFFFDFA889FF2240F400704B +:10D21000A881706890F864101046FBF730F89AF80F +:10D2200012004FF00109002C00F0F080FAF77DFEAB +:10D23000FAF76BFE90B99AF8120078B1686A4178F3 +:10D2400061B100789AF80710C0F3C000884205D198 +:10D2500085F80290BDE8F05F00F037B9686A417860 +:10D260002981002908BFAF6203D0286BF6F70AFABC +:10D27000A862A88940F02000A881EF70706800F1D2 +:10D28000700B044690F82C0001281BD1FBF757FCCB +:10D2900059462046F3F729FEA0B13078002870687F +:10D2A0000CBF00F59A7000F50170218841809BF851 +:10D2B000081001719BF80910417180F80090E8791D +:10D2C000E5F722FE686A9AF806100078C0F380003D +:10D2D00088423AD0706800F1700490F87500002818 +:10D2E0002FD002284AD06771307800281CBF2079DF +:10D2F000002809D027716A89394642F010026A81F4 +:10D300006A694FF010009047E078A0B1E770FCF731 +:10D31000EAF8002808BFFFDF08206A89002142F0F0 +:10D3200008026A816A699047D4E91210491C40F1E9 +:10D330000000C4E91210A07901280CBFA77184F87D +:10D340000690A88940F48070A881696A9AF807302D +:10D350000878C0F3C0029A424DD1726800F0030011 +:10D3600002F17004012818BF02282DD003281CBF29 +:10D37000687940F0040012D068713CE0E86AF6F782 +:10D38000BCF8002808BFFFDFD4E91210491C40F1A7 +:10D390000000C4E91210E879E5F7B6FDA3E784F8C8 +:10D3A0000290AA89484642F40062AA816A8942F042 +:10D3B00001026A816A699047E079012801D1E77129 +:10D3C00019E084F8079016E04878D8B1A98941F4AB +:10D3D0000061A981A96A71B1FB2884BF687940F016 +:10D3E0001000C9D8A879002808BFC84603D08020FB +:10D3F0006A69002190470120A9698847E0B36879EC +:10D40000A0B13AE0E0790128DBD1D8E7002818BFC5 +:10D41000FAF7C5FDA88940F04000A881E97801200D +:10D42000491CC9B2E97001292DD8E5E7307890B9D7 +:10D430003C48007810F0100F04D13B49097811F0F6 +:10D44000100F1AD06989B9B9A96A21B9298911B10E +:10D4500010F0100F11D0B8F1000F1CBF0120FFF722 +:10D46000D1FDFFF74BFBB8F1000F08BFBDE8F09FFF +:10D470000220BDE8F05FC5E5FFE7B8F1000F1CBF73 +:10D480000020FFF7BFFDBDE8F05F00F01EB870B5EB +:10D490000D4606462248224900784C6850B1FAF7FA +:10D4A000F7FD034694F8642029463046BDE87040F5 +:10D4B000FDF78BBAFAF7ECFD034694F86420294691 +:10D4C0003046BDE8704004F0FCBE154910B54C680C +:10D4D000FBF714FBFBF7F3FAFBF7BCF9FBF768FA71 +:10D4E000FAF7FEFC94F82C00012808BFFBF727FB95 +:10D4F00094F86F0038B9A06E28B1002294F86E003D +:10D500001146F8F7F0FB094C00216269A0899047A9 +:10D51000E2696179A07890470020207010BD00007A +:10D520005801002032010020300D0020240100208D +:10D530002DE9F047FA4F894680463D782C0014D0FB +:10D540000126012D11DB601EC4B207EBC40090F868 +:10D550005311414506D10622494600F5AA70F0F75D +:10D560003FFF28B1761CAE42EDDD1020BDE8F0870C +:10D570002046BDE8F087EA498A78824286BF08449F +:10D5800090F843010020704710B540F2D3120021FB +:10D59000E348F0F77CFF0822FF21E248F0F777FF2D +:10D5A000E1480021417081704FF46171818010BDAC +:10D5B0002DE9F0410E460546FFF7BAFFD84C10287A +:10D5C00016D004EBC00191F85A0110F0010F1CBFF6 +:10D5D0000120BDE8F081607808283CBF012081F877 +:10D5E0005A011CD26078401C60700120BDE8F081B7 +:10D5F0006078082813D222780127501C207004EB91 +:10D60000C2083068C8F85401B088A8F85801102A38 +:10D6100028BFFFDF88F8535188F85A71E2E70020ED +:10D62000BDE8F081C04988707047BF488078704776 +:10D630002DE9F041BA4D00272878401E44B2002C55 +:10D6400030DB00BF05EBC40090F85A0110F0010F69 +:10D6500024D06878E6B2401E687005EBC6083046F4 +:10D6600088F85A7100F0E8FA102817D12878401E7F +:10D67000C0B22870B04211D005EBC001D1F85301FF +:10D68000C8F85301D1F85701C8F85701287800F0BD +:10D69000D3FA10281CBF284480F80361601E44B2EE +:10D6A000002CCFDAA0488770BDE8F0819C498A78C9 +:10D6B000824286BF01EB0010C01C002070472DE99C +:10D6C000F0470127994690463D460026FFF730FF78 +:10D6D000102820D0924C04EBC00191F85A1101F0AF +:10D6E000010600F0A9FA102815D0B9F1000F18BFF3 +:10D6F00089F80000A17881420DD904EB001111F1E5 +:10D70000030F08D0204490F84B5190F83B010128BA +:10D710000CBF0127002748EA060047EA0501084038 +:10D72000BDE8F0872DE9F05F1F4690468946064622 +:10D73000FFF7FEFE7A4C054610282ED000F07CFA4A +:10D7400010281CBF1220BDE8F09FA07808283ED208 +:10D75000A6781022701CA07004EB061909F10300D2 +:10D760004146F3F768FB09F1830010223946F3F7CD +:10D7700062FB10213846F3F74BFB3444102184F848 +:10D7800043014046F3F744FB84F84B0184F803510E +:10D79000002084F83B01BDE8F09FA078082816D24D +:10D7A00025784FF0000A681C207004EBC50BD9F8EF +:10D7B0000000CBF85401B9F80400ABF85801102D63 +:10D7C00028BFFFDF8BF853618BF85AA1C0E7072011 +:10D7D000BDE8F09F2DE9F041514CA078401E45B2C4 +:10D7E000002DB8BFBDE8F081EAB2A078401EC1B2FA +:10D7F000A17054FA85F090F803618A423DD004EBA1 +:10D80000011004EB0213D0F803C0C3F803C0D0F832 +:10D8100007C0C3F807C0D0F80BC0C3F80BC0D0F8DE +:10D820000FC0C3F80FC0D0F883C0C3F883C0D0F8CE +:10D8300087C0C3F887C0D0F88BC0C3F88BC0D0F8BE +:10D840008F00C3F88F006318A01801EB410193F813 +:10D8500003C102EB420204EB410180F803C104EB77 +:10D860004202D1F80BC1C2F80BC1B1F80F11A2F8F6 +:10D870000F1193F83B1180F83B1104EBC60797F8A2 +:10D880005A0110F0010F1CD1304600F0D5F91028D4 +:10D8900017D12078401EC0B22070B04211D004EBE6 +:10D8A000C000D0F85311C7F85311D0F85701C7F88A +:10D8B0005701207800F0C0F910281CBF204480F8E0 +:10D8C0000361681E45B2002D8EDABDE8F08116496D +:10D8D0004870704714484078704738B14AF2B81120 +:10D8E000884203D810498880012070470020704783 +:10D8F0000D488088704710B5FFF71AFE102804D035 +:10D9000000F09AF9102818BF10BD082010BD044976 +:10D910008A78824286BF01EB001083300020704776 +:10D92000600F00206C01002060010020FE4B93F886 +:10D9300002C084459CBF00207047184490F8030142 +:10D9400003EBC00090F853310B70D0F85411116004 +:10D95000B0F85801908001207047F34A114491F8C3 +:10D960000321F2490A700268C1F8062080884881C4 +:10D97000704770B516460C460546FBF7D5F8FAF722 +:10D98000C4F9EA48407868B1E748817851B12A196A +:10D99000002E0CBF8330C01CFAF791F9FAF7D8F9C2 +:10D9A000012070BD002070BD10B5FAF7FFF9002806 +:10D9B00004BFFF2010BDBDE81040FAF71DBAFAF70A +:10D9C000F5B9D9498A7882429CBF00207047084443 +:10D9D00090F8030101EBC00090F85A0100F001003B +:10D9E00070472DE9F047D04D00273E4628780028A3 +:10D9F00086BF4FF01009DFF83883BDE8F087AC78B8 +:10DA000021000CD00122012909DB601EC4B22819B3 +:10DA100090F80331B34203D0521C8A42F5DD4C46E4 +:10DA2000A14286BF05EB0410C01C002005EBC60A0E +:10DA30009AF85A1111F0010F16D050B1102C04D0E1 +:10DA4000291991F83B11012903D01021F3F7E0F9CE +:10DA500050B108F8074038467B1C9AF853210AF564 +:10DA6000AA71DFB2FAF7B5FC701CC6B22878B042D2 +:10DA7000C5D8BDE8F0872DE9F041AB4C002635460E +:10DA8000A07800288CBFAA4FBDE8F0816119C0B210 +:10DA900091F80381A84286BF04EB0510C01C00204A +:10DAA00091F83B11012903D01021F3F7B1F958B1D6 +:10DAB00004EBC800BD5590F8532100F5AA7130461B +:10DAC000731CDEB2FAF785FC681CC5B2A078A842C8 +:10DAD000DCD8BDE8F0810144934810B500EB02109A +:10DAE0000A4601218330FAF7EAF8BDE81040FAF758 +:10DAF0002FB90A468D4910B5497841B18A4B9978BA +:10DB000029B10244D81CFAF7DAF8012010BD002030 +:10DB100010BD854A01EB410102EB41010268C1F8E9 +:10DB20000B218088A1F80F0170472DE9F0417E4D4F +:10DB300007460024A878002898BFBDE8F081C0B24D +:10DB4000A04217D905EB041010F1830612D0102162 +:10DB50003046F3F75DF968B904EB440005EB400883 +:10DB600008F20B113A463046FBF74EFDB8F80F01AC +:10DB7000A8F80F01601CC4B2A878A042DFD8BDE8A5 +:10DB8000F081014610226B48F3F755B96948704798 +:10DB900065498A78824203D90A1892F843210AB16A +:10DBA0000020704700EB400001EB400000F20B103A +:10DBB00070475D498A78824206D9084490F83B0153 +:10DBC000002804BF01207047002070472DE9F04174 +:10DBD0000E460746144606213046F3F719F9524D12 +:10DBE00098B1A97871B105F59D7011F0010F18BFBA +:10DBF00000F8014FA978490804D0447000F8024F9A +:10DC0000491EFAD10120BDE8F08138463146FFF7C0 +:10DC10008FFC10280CD000F00FF8102818BF08282F +:10DC200006D0284480F83B414FF00100BDE8F08168 +:10DC30004FF00000BDE8F0813B4B10B4844698786B +:10DC400001000ED0012201290BDB401EC0B21C18BE +:10DC500094F80341644504BF10BC7047521C8A42CB +:10DC6000F3DD10BC1020704770B52F4C01466218D0 +:10DC7000A078401EC0B2A07092F8035181423CD0FF +:10DC800004EB011304EB001C01EB4101DCF8036021 +:10DC9000C3F80360DCF80760C3F80760DCF80B60CA +:10DCA000C3F80B60DCF80F60C3F80F60DCF883602A +:10DCB000C3F88360DCF88760C3F88760DCF88B60AA +:10DCC000C3F88B60DCF88FC0C3F88FC0231800EB5B +:10DCD000400093F803C104EB400082F803C104EB59 +:10DCE0004101D0F80BC1C1F80BC1B0F80F01A1F888 +:10DCF0000F0193F83B0182F83B0104EBC50696F84F +:10DD00005A0110F0010F18BF70BD2846FFF794FFAD +:10DD1000102818BF70BD2078401EC0B22070A842E5 +:10DD200008BF70BD08E00000600F00206001002007 +:10DD30006C0100203311002004EBC000D0F8531117 +:10DD4000C6F85311D0F85701C6F857012078FFF7ED +:10DD500073FF10281CBF204480F8035170BD0000E1 +:10DD60004078704730B50546007801F00F0220F08A +:10DD70000F0010432870092912D2DFE801F00507CF +:10DD800005070509050B0F0006240BE00C2409E02C +:10DD9000222407E001240020E87003E00E2401E0C3 +:10DDA0000024FFDF6C7030BD007800F00F0070477A +:10DDB0000A68C0F803208988A0F807107047D0F8D7 +:10DDC00003200A60B0F80700888070470A68C0F82E +:10DDD00009208988A0F80D107047D0F809200A6042 +:10DDE000B0F80D00888070470278402322F040028E +:10DDF00003EA81111143017070470078C0F380106D +:10DE000070470278802322F0800203EAC111114397 +:10DE1000017070470078C009704770B514460E460F +:10DE200005461F2A88BFFFDF2246314605F109005B +:10DE3000F0F703FBA01D687070BD70B544780E4606 +:10DE40000546062C38BFFFDFA01F84B21F2C88BFF9 +:10DE50001F24224605F109013046F0F7EEFA20466C +:10DE600070BD70B514460E4605461F2A88BFFFDFF9 +:10DE70002246314605F10900F0F7DFFAA01D68706F +:10DE800070BD0968C0F80F1070470A88A0F8132009 +:10DE900089784175704790F8242001F01F0122F025 +:10DEA0001F02114380F824107047072988BF0721FB +:10DEB00090F82420E02322F0E00203EA411111430C +:10DEC00080F8241070471F3008F065B810B504467C +:10DED00000F000FB002818BF204410BDC17811F0ED +:10DEE0003F0F1BBF027912F0010F0022012211F037 +:10DEF0003F0F1BBF037913F0020F002301231A44C5 +:10DF000002EB4202530011F03F0F1BBF027912F0E7 +:10DF1000080F0022012203EB420311F03F0F1BBF49 +:10DF2000027912F0040F00220122134411F03F0F76 +:10DF30001BBF027912F0200F0022012202EBC20265 +:10DF400003EB420311F03F0F1BBF027912F0100FD9 +:10DF50000022012202EB42021A4411F03F0F1BBFC4 +:10DF6000007910F0400F00200120104410F0FF0055 +:10DF700014BF012100210844C0B2704770B5027877 +:10DF8000417802F00F02082A4DD2DFE802F00408BF +:10DF90000B4C4C4C0F14881F1F280AD943E00C2946 +:10DFA00007D040E0881F1F2803D93CE0881F1F28A6 +:10DFB00039D8012070BD4A1EFE2A34D88446C07864 +:10DFC00000258209032A09D000F03F04601C884222 +:10DFD00004D86046FFF782FFA04201D9284670BDF1 +:10DFE0009CF803004FF0010610F03F0F1EBF1CF11C +:10DFF0000400007810F0100F13D06446042160462E +:10E0000000F068FA002818BF14EB0000E6D0017891 +:10E0100001F03F012529E1D280780221B1EB501FA8 +:10E02000DCD3304670BD002070BD70B5017801258D +:10E0300001F00F01002404290AD007290DD0082976 +:10E040001CBF002070BD40780E2836D0204670BD21 +:10E050004078801F1F2830D9F8E7844640789CF824 +:10E0600003108A09032AF1D001F03F06711C814296 +:10E07000ECD86046FFF732FFB042E7D89CF80300C7 +:10E0800010F03F0F1EBF1CF10400007810F0100FBD +:10E0900013D066460421604600F01CFA002818BF21 +:10E0A00016EB0000D2D0017801F03F012529CDD236 +:10E0B00080780221B1EB501FC8D3284670BD10B440 +:10E0C000017801F00F01032920D0052921D14478DE +:10E0D000B0F81910B0F81BC0B0F81730827D222CB0 +:10E0E00017D1062915D3B1F5486F98BFBCF5FA7F53 +:10E0F0000FD272B1082A98BF8A420AD28B429CBFC3 +:10E10000B0F81D00B0F5486F03D805E040780C2842 +:10E1100002D010BC0020704710BC012070472DE9D0 +:10E12000F0411F4614460D00064608BFFFDF21469A +:10E13000304600F0CFF9040008BFFFDF30193A463F +:10E140002946BDE8F041F0F778B9C07800F03F000B +:10E150007047C02202EA8111C27802F03F021143E7 +:10E16000C1707047C07880097047C9B201F00102E0 +:10E17000C1F340031A4402EB4202C1F3800303EBF4 +:10E180004202C1F3C00302EB4302C1F3001303EBED +:10E1900043031A44C1F3401303EBC30302EB4302EE +:10E1A000C1F380131A4412F0FF0202D0521CD2B203 +:10E1B0000171C37802F03F0103F0C0031943C1703D +:10E1C000511C417070472DE9F0410546C078164654 +:10E1D00000F03F041019401C0F46FF2888BFFFDFE6 +:10E1E000281932463946001DF0F727F9A019401CBE +:10E1F0006870BDE8F081C178407801F03F01401AB5 +:10E20000401E80B2704710B590F803C00B460CF06A +:10E210003F0144780CF03F0CA4EB0C0CACF1010C6A +:10E220001FFA8CF4944288BF14462BB10844011D98 +:10E2300022461846F0F701F9204610BD4078704795 +:10E2400000B5027801F0030322F003021A430270C2 +:10E25000012914BF0229002104D0032916BFFFDFC2 +:10E26000012100BD417000BD00B5027801F003033B +:10E2700022F003021A430270012914BF022900216F +:10E2800004D0032916BFFFDF012100BD417000BD8E +:10E29000007800F003007047417841B1C078192838 +:10E2A00003D2BC4A105C884201D101207047002093 +:10E2B000704730B501240546C17019293CBFB548E7 +:10E2C000445C02D3FF2918BFFFDF6C7030BD70B50E +:10E2D00015460E4604461B2A88BFFFDF65702A4696 +:10E2E0003146E01CBDE87040F0F7A7B8B0F8070071 +:10E2F0007047B0F809007047C172090A017370478E +:10E30000B0F80B00704730B4B0F80720B0F809C07F +:10E31000B0F805300179941F40F67A45AC4298BFB9 +:10E32000BCF5FA7F0ED269B1082998BF914209D293 +:10E3300093429FBFB0F80B00B0F5486F012030BC8E +:10E3400098BF7047002030BC7047001D07F023BE07 +:10E35000021D0846114607F01EBEB0F809007047BE +:10E36000007970470A684260496881607047426876 +:10E370000A60806848607047098881817047808999 +:10E38000088070470A68C0F80E204968C0F812106B +:10E390007047D0F80E200A60D0F81200486070472D +:10E3A0000968C0F816107047D0F81600086070476A +:10E3B0000A68426049688160704742680A60806804 +:10E3C000486070470968C1607047C068086070475E +:10E3D000007970470A684260496881607047426806 +:10E3E0000A608068486070470171090A417170478E +:10E3F0008171090AC17170470172090A417270473F +:10E400008172090AC172704780887047C08870475E +:10E41000008970474089704701891B2924BF4189C1 +:10E42000B1F5A47F07D381881B2921BFC088B0F52F +:10E43000A47F01207047002070470A684260496845 +:10E440008160704742680A6080684860704701795F +:10E4500011F0070F1BBF407910F0070F00200120BB +:10E460007047017911F0070F1BBF407910F0070FBB +:10E470000020012070470171704700797047417199 +:10E480007047407970478171090AC1717047C0882F +:10E4900070470179407901F007023F498A5C012AFF +:10E4A00006D800F00700085C01289CBF01207047D7 +:10E4B00000207047017170470079704741717047C3 +:10E4C0004079704730B50C460546FB2988BFFFDF11 +:10E4D0006C7030BDC378024613F03F0008BF704730 +:10E4E0000520127903F03F0312F0010F37D0002905 +:10E4F00014BF0B20704700BF12F0020F32D0012969 +:10E5000014BF801D704700BF12F0040F2DD00229E8 +:10E5100014BF401C704700BF12F0080F28D0032919 +:10E5200014BF801C704700BF12F0100F23D00429C5 +:10E5300014BFC01C704700BF12F0200F1ED0052969 +:10E540001ABF1230C0B2704712F0400F19D006291E +:10E550001ABF401CC0B27047072918D114E0002927 +:10E56000CAD114E00129CFD111E00229D4D10EE0A3 +:10E570000329D9D10BE00429DED108E00529E3D134 +:10E5800005E00629E8D102E0834288BF70470020F9 +:10E5900070470000246302001C63020030B490F84E +:10E5A00064508C88B1F808C015F00C0F1BD000BF68 +:10E5B000B4F5296F98BF4FF4296490F8655015F0B1 +:10E5C0000C0F17D0BCF5296F98BF4FF4296C4A88FF +:10E5D000C988A0F84420A0F84810A0F84640A0F848 +:10E5E0004AC030BC7047002B1CBF157815F00C0FCB +:10E5F000DED1E2E7002B1CBF527812F00C0FE1D104 +:10E60000E5E7DDF800C08181C2810382A0F812C075 +:10E6100070471B2202838282C281828142800281F2 +:10E62000028042848284828359B14FF429614183FC +:10E63000C18241820182C18041818180C184018582 +:10E6400070474FF4A4714183C18241820182C1802D +:10E6500041818180C18401857047F0B4B0F84820C1 +:10E66000818F468EC58E8A4228BF0A4690F8651073 +:10E670004FF0000311F00C0F18BF4FF4296106D1C1 +:10E68000B0F84AC0B0F840108C4538BF61464286A9 +:10E69000C186048FB0F83AC0944238BF14468C4506 +:10E6A00038BF8C460487A0F83AC0B2420ABFA942DC +:10E6B0004FF0010C4FF0000C058EB0F84410C28FE3 +:10E6C000848E914228BF114690F8642012F00C0FFE +:10E6D00018BF4FF4296206D1B0F84660B0F8422066 +:10E6E000964238BF324690F85A60022E0AD0018610 +:10E6F0008286A9420ABFA2420120002040EA0C0003 +:10E70000F0BC70478D4238BF2946944238BF22463C +:10E7100080F85A30EBE7508088899080C889D08093 +:10E72000088A1081488A508101201070704730B4E7 +:10E7300002884A80B0F830C0A1F804C0838ECB8034 +:10E74000428E0A81C48E4C81B0F85650A54204BF57 +:10E75000B0F85240944208D1B0F858409C4202BFF1 +:10E76000B0F854306345002301D04FF001030B7320 +:10E7700000F13003A0F852201A464B89D3848B88CD +:10E780009384CA88A0F858204FF00100087030BC6C +:10E79000704730B404460A46088E91F864104FF46E +:10E7A000747311F00C0F1CBF03EB801080B21ED0ED +:10E7B000918E814238BF0846118F92F865C01CF0D7 +:10E7C0000C0F1CBF03EB811189B218D0538F8B4201 +:10E7D00038BF194692F866301CF00C0F08BF0023B2 +:10E7E000002C0CBF0122002230BCF2F798BC022999 +:10E7F00007BF80003C30C000703080B2D8E7BCF169 +:10E80000020F07BF89003C31C900703189B2DDE7D2 +:10E810002DE9F041044606F099FCC8B9FE4F78682E +:10E8200090F8221001260025012914D00178012931 +:10E830001BD090F8281001291CBF0020BDE8F081F2 +:10E84000657018212170D0F82A10616080F8285076 +:10E850000120BDE8F081657007212170416A616087 +:10E8600080F822500120BDE8F081657014212170EC +:10E87000811C2022201DEFF7E0FD257279680D70C4 +:10E8800081F82850E54882888284C26B527B80F8E8 +:10E89000262080F82260C86B0088F5F738FCF5F771 +:10E8A000E0F8D5E7DC4840680178002914BF80888B +:10E8B0004FF6FF70704730B5D74C83B00D462078C7 +:10E8C0007F2808BFFFDF94F900307F202070D4F844 +:10E8D00004C09CF85000062808BF002205D09CF810 +:10E8E000500008280CBF022201229CF85400CDE9F8 +:10E8F000000302929CF873309CF880200CF13201E6 +:10E90000284606F08FFC03B0BDE8304006F01FBE7D +:10E910002DE9F04106F05FFC002818BF06F0E4FB8B +:10E92000BD4C606800F1840290F87610895C80F834 +:10E930008010002003F07EF828B3FAF753F86068DF +:10E94000B74990F855000D5C2846F9F7A3FD6068BB +:10E950004FF0000680F8735090F8801011F00C0F03 +:10E960000CBF25200F20F9F76CFC606890F8801030 +:10E970000120F9F711FE606890F84010032918BFD4 +:10E9800002290FD103E0BDE8F04101F02FB990F862 +:10E9900076108430085C012804D101221146002041 +:10E9A000FAF707F9FAF7D5F8606890F88050012D6A +:10E9B00007BF0127032100270521A068FFF799F869 +:10E9C000616881F8520040B1002F18BF402521D066 +:10E9D000F9F787F92846FAF79DF86068806DFAF72D +:10E9E0000DF8606890F85410FF291CBF6D30FEF7D9 +:10E9F000B4FFFF21606880F8531080F8541080F84D +:10EA0000626080F8616080F87D60062180F85010B7 +:10EA1000BDE8F08115F00C0F14BF55255025D7E740 +:10EA200070B57D4C0646606800F150052046806850 +:10EA300041B1D0F80510C5F81D10B0F80900A5F8CF +:10EA4000210003E005F11D01FFF7B9F9A068FFF708 +:10EA5000D4F985F82400A0680021032E018002D09B +:10EA6000052E04D03DE00321FFF77CF939E00521B4 +:10EA7000FFF778F96068C06B00F10E01A068FFF73E +:10EA800000FA6068C06B00F11201A068FFF7FDF9A1 +:10EA9000D4E90110CA6B527D8275CA6BD28AC275E5 +:10EAA000120A0276CA6B52884276120A8276CA6BC2 +:10EAB0009288C276120A0277CA6BD2884277120A0B +:10EAC0008277C96B0831FFF7FEF96068C06B017E81 +:10EAD000A068FFF7E0F9606890F88610A068FFF77B +:10EAE000E4F905F11D01A068FFF770F995F824100D +:10EAF000A068FFF786F9606800F1320590F8316090 +:10EB000090F8511091B190F84010032906D190F877 +:10EB10003910002918BF90F8560001D190F8530021 +:10EB2000FFF736F800281CBF012605462946A068D5 +:10EB3000FFF73EF93146A068BDE87040FFF754B9D1 +:10EB40003549496881F84B00704770B5324D002453 +:10EB50000126A8606968A1F8814081F8834081F8A6 +:10EB6000506091F85020022A1FBF91F850100129DF +:10EB7000FFDF70BD06F0CDFA6868047080F82240AF +:10EB800080F8284090F8520030B1F9F7CDFFF9F73E +:10EB9000BCF8686880F852406868072180F84A40ED +:10EBA00080F8396080F8404080F8554080F84B404C +:10EBB00080F87D4080F8381070BD2DE9F041164C8A +:10EBC000054686B0606890F85000012818BF0228FA +:10EBD00005D003281EBF0C2006B0BDE8F081687A7E +:10EBE000022839D0F9F76FFB0220F9F701FF0D4930 +:10EBF00001F10C0090E80D108DE80D10D1E907012E +:10EC0000CDE904016846F9F7E1FE606890F94B0030 +:10EC1000F9F732FCA06807E07401002044110020DD +:10EC20004363020040630200F9F7E5FEFC48F9F790 +:10EC3000B9FEFC48F9F726FC606890F831103230D4 +:10EC4000F9F7A5FB0F210720F9F7BFFB606890F8E3 +:10EC50003900E0B1FEF70FFF6168287A01F1840204 +:10EC600081F87600287A805C81F880006868886581 +:10EC70002A68CA65687A68B1012824D00525022867 +:10EC800008BF81F850506FD0032878D080E0FEF79D +:10EC9000A8FEE1E7E44B91F83850002291F85500C6 +:10ECA000401CA3FB006C4FEA5C0CACEB8C0C60448A +:10ECB00081F8550025FA00F010F0010F03D1501C27 +:10ECC000C2B2032AEAD3002681F87D6091F8490098 +:10ECD000002804BF91F85100002841D0F7F744FA0A +:10ECE000074660683946406CF7F736FFDFF83C832B +:10ECF000054690FBF8F008FB105041423846F6F705 +:10ED00001AFF6168486495FBF8F08A6F10448867C1 +:10ED1000FEF7EEFD01466068826F914220D847649D +:10ED2000866790F8510000281CBF0120FEF7FDFE09 +:10ED30000121606890F84A20002A1CBF90F8492001 +:10ED4000002A0DD090F8313000F13202012B04D1AD +:10ED5000527902F0C002402A08D03230FAF78CFC17 +:10ED60006168042081F8500012E008E00125FEF7F8 +:10ED70000DFF61682A463231FAF746FCF0E7002AB7 +:10ED800018BFFFDF012000F089FF606880F8505055 +:10ED900006B00020BDE8F08170B5A54D686890F818 +:10EDA000501004292ED005291CBF0C2070BD90F8EE +:10EDB0007D100026002990F883104FEA511124D0CD +:10EDC000002908BF012407D0012908BF022403D06D +:10EDD000022914BF00240824C06D00281CBF002095 +:10EDE00000F05CFF6868806DF9F708FE686890F8CD +:10EDF0004010022943D0032904BF90F86C10012968 +:10EE000041D04DE0FFF784FD52E0002908BF012406 +:10EE100007D0012908BF022403D0022914BF00240F +:10EE20000824C06D00281CBF002000F037FF686870 +:10EE3000806DF9F7E3FD686890F84010022906D06C +:10EE4000032904BF90F86C10012904D010E090F859 +:10EE50006C1002290CD1224614F00C0F04D090F84B +:10EE60004C00012808BF042201210020F9F7A1FE6F +:10EE70006868072180F8804080F8616016E090F8AB +:10EE80006C1002290CD1224614F00C0F04D090F81B +:10EE90004C00012808BF042201210020F9F789FE57 +:10EEA0006868082180F8804080F8616080F8501020 +:10EEB000002070BD5E49002210F0010F496802D0A9 +:10EEC000012281F8842010F0080F03D0114408209B +:10EED00081F88400002070475549496881F848004E +:10EEE000704710B5524C636893F83030022B14BF52 +:10EEF000032B00280BD100291ABF02290120002072 +:10EF00001146FEF7F8FC08281CBF012010BD606800 +:10EF100090F83000002816BF022800200120BDE82C +:10EF20001040FAF731BB4248406890F830000028A2 +:10EF300016BF022800200120FAF726BB3C49496889 +:10EF400081F8300070473A49496881F84A007047B3 +:10EF500070B5374C616891F83000002816BF022860 +:10EF60000020012081F8310001F13201FAF7F6FAB0 +:10EF7000606890F83010022916BF03290121002192 +:10EF800080F8511090F8312000F132034FF0000565 +:10EF9000012A04BF5B7913F0C00F0AD000F13203DD +:10EFA000012A04D15A7902F0C002402A01D000227D +:10EFB00000E0012280F84920002A04BF002970BD2A +:10EFC0008567F7F7D1F86168486491F85100002827 +:10EFD0001CBF0020FEF7A9FD0026606890F84A10CB +:10EFE00000291ABF90F84910002970BD90F831200F +:10EFF00000F13201012A04D1497901F0C001402910 +:10F0000005D02946BDE870403230FAF735BBFEF72F +:10F01000BDFD61683246BDE870403231FAF7F4BA9E +:10F020004063020046630200ABAAAAAA40420F0056 +:10F030007401002070B5FF4D0C4600280CBF012361 +:10F040000023696881F8393081F842004FF00800E8 +:10F0500081F856000CD1002C1ABF022C0120002090 +:10F060001146FEF748FC6968082881F8560001D06F +:10F07000002070BD022C14BF032C1220F8D170BDEB +:10F08000002818BF112070470328EA4A526808BFB9 +:10F09000D16382F840000020704710B5E54C6068ED +:10F0A00090F8401003291CBF002180F8601001D0A7 +:10F0B000002010BD0123C16B1A460020F2F738F87A +:10F0C0006168CA6B526A904294BF0120002081F8A7 +:10F0D0006000EDE7D748416891F84000032804D06C +:10F0E000012818BF022807D004E091F84200012847 +:10F0F00008BF70470020704791F84100012814BFF5 +:10F1000003280120F6D1704770B5F9F7F7FCF9F73D +:10F11000D6FCF9F79FFBF9F74BFCC64C002560685D +:10F1200090F8520030B1F9F7FFFCF8F7EEFD606897 +:10F1300080F8525060680121A0F8815080F8835017 +:10F1400080F8501080F82850002070BDB94810B5E4 +:10F150004068643006F0B1FB002010BDB5480121C5 +:10F16000406890F84020032A03BF80F82A10C26B41 +:10F170001288002218BF80F82A20828580F8281083 +:10F180007047AC49496881F88600704701780023D0 +:10F1900011F0010FA749496809D04278032A08BF36 +:10F1A000CB6381F84020012281F884201346027845 +:10F1B00012F0040F0CD082784FF0000C032A08BF25 +:10F1C000C1F83CC081F840200B44082283F8842019 +:10F1D000C27881F830200279002A16BF022A012362 +:10F1E000002381F8393081F84120427981F83820B4 +:10F1F000807981F848004FF0000070478D484068E2 +:10F200008030704770B58B4C06460D46606890F8AC +:10F210005000032818BFFFDF022E1EBF032EFFDFA2 +:10F2200070BD002D18BF06F0A1F900216068A0F89C +:10F23000811080F88310012180F8501070BD00F01B +:10F24000D5BC2DE9F0477B4C0646894660684FF0F7 +:10F250000108072E90F8397038BF032540D3082ED7 +:10F2600084BF0020BDE8F08790F85010062908BF41 +:10F27000002105D090F8501008290CBF022101216F +:10F2800090F8800005F0AEFF002873D1A068C17827 +:10F2900011F03F0F12D0027912F0010F0ED0616809 +:10F2A0004FF0050591F85220002A18BFB9F1000F60 +:10F2B00016D091F88010012909D011E011F03F0F0C +:10F2C0001ABF007910F0100F002F53D14CE04FF00F +:10F2D00001024FF00501FEF74CFB616881F8520016 +:10F2E000A16808782944C0F3801030B1487900F053 +:10F2F000C000402808BF012000D00020616891F8BC +:10F300005210002918BF002807D0FEF74DFB014618 +:10F31000606880F8531080F86180606890F853103E +:10F32000FF292AD080F854100846FEF74AFB40EA2D +:10F330000705606890F85320FF2A18BF002D10D0F1 +:10F34000072E0ED3A068C17811F03F0F09D00179C4 +:10F3500011F0020F05D00B21FEF7BDFB606880F8AD +:10F3600062802846BDE8F087FEF75FF9002808BFF5 +:10F37000BDE8F0870120BDE8F087A36890F8392048 +:10F3800059191B78C3F3801C00F153036046FEF744 +:10F3900096F90546CDE72DE9F043264C87B0A068E5 +:10F3A000FEF7E0FE7F264FF00108002558B1022746 +:10F3B00001287DD0022800F0EF80F9F74BFA07B062 +:10F3C0000620BDE8F083F9F745FA616891F840003E +:10F3D000032800F01581A068C27812F03F0F05D015 +:10F3E000037913F0100F18BF012700D10027002F59 +:10F3F00014BF0823012312F03F0F00F001810079B0 +:10F4000033EA000240F0FC8010F0020F08D091F8BF +:10F410008000002105F064FE002808BF012000D014 +:10F4200000208DF80C508DF810508DF814504FF0CE +:10F43000FF0801E074010020D0B105AA03A904A8C7 +:10F4400000F07AFC606890F831809DF80C0000288C +:10F4500018BF48F002080BD1A068FEF7DBFC81461C +:10F460000121A068FEF732FD4946F8F79AFF28B35C +:10F47000FFB1012000F0DDFB002852D020787F286A +:10F4800008BFFFDF94F900102670606890F85420E0 +:10F49000CDE90021029590F8733090F8802000F1BA +:10F4A0003201404605F0BEFE606880F86C50A3E073 +:10F4B00038E041460020FFF7FEF9A1E0606890F8CF +:10F4C0004100032818BF02282BD19DF81000002806 +:10F4D00027D09DF80C00002823D1F7B1012000F0BF +:10F4E000A8FB00281DD020787F2808BFFFDF94F9F3 +:10F4F00000102670606890F85420CDE90021029534 +:10F5000090F8733090F8802000F13201FE2005F071 +:10F5100089FE606880F86C506EE0FE210020FFF7E5 +:10F52000CAF96DE0F9F796F9A0681821C27812F0CF +:10F530003F0F65D00279914362D10421FEF7C6FCEA +:10F54000616891F84020032A01BF8078B7EB501F13 +:10F5500091F86000002853D04FF0010000F069FBE3 +:10F56000E8B320787F2808BFFFDF94F900102670E9 +:10F57000606890F85420CDE90021029590F873302E +:10F5800090F8802000F13201FF2005F04BFE60680A +:10F5900080F86C8030E000BFF9F75CF9606890F8A3 +:10F5A000400003282CD0A0681821C27812F03F0F29 +:10F5B00026D0007931EA000022D1012000F039FB89 +:10F5C00068B120787F2808BFFFDF94F9001026700B +:10F5D000606890F85420CDE90021029500E00FE02A +:10F5E00090F8733090F8802000F13201FF2005F090 +:10F5F00019FE606880F86C7007B00320BDE8F083E6 +:10F6000007B00620BDE8F083F0B5FE4C074683B096 +:10F6100060686D460078002818BFFFDF002661682B +:10F620008E70C86B02888A8042884A8382888A8367 +:10F63000C088C88381F8206047B10121A068FEF727 +:10F6400045FC0546A0680078C10907E06946A06846 +:10F65000FEF7B5FBA0680078C0F380116068012751 +:10F6600090F85120002A18BF002904D06A7902F0CE +:10F67000C002402A26D090F84A20002A18BF00294C +:10F6800003D0697911F0C00F1CD000F10E00E3F730 +:10F69000B3FC616891F85400FF2819D001F1080209 +:10F6A000C91DFEF743F9002808BFFFDF6068C17974 +:10F6B00041F00201C171D0F86D104161B0F87110D4 +:10F6C00001830FE02968C0F80E10A9884182E0E7A5 +:10F6D000C86B427ECA71D0F81A208A60C08B8881BC +:10F6E0004E610E8360680770C26B90F84B1082F811 +:10F6F0006710C06B0088F4F70AFDF4F7A3F903B0B4 +:10F70000F0BD2DE9F041BF4C0546002760684FF081 +:10F7100001083E4690F84000012818BF022802D098 +:10F72000032818BFFFDF5DB1A068FEF727FC18B9FA +:10F73000A068FEF77AFC18B100F08FFB074645E0A1 +:10F74000606890F850007F25801F06283ED2DFE8D1 +:10F7500000F003191924352FAA48F9F709FA0028EF +:10F7600008BF2570F9F7EBF9606890F8520030B1E6 +:10F77000F9F7DAF9F8F7C9FA606880F85260F9F732 +:10F7800069F830E09F48F9F7F3F9002808BF2570C1 +:10F79000F9F7D5F905F0EAFEC3E09A48F9F7E8F978 +:10F7A000002808BF2570F9F7CAF9F9F753F81AE0ED +:10F7B0009448F9F7DDF930B9257004E09148F9F77C +:10F7C000D7F90028F8D0F9F7BAF9AAE0102F80F09D +:10F7D0003881DFE807F01E9DA6AAF1F108B3F2F127 +:10F7E000F1F10C832051BDE8F041FFF791B80320FF +:10F7F00002F020F9002870D000210320FFF710F953 +:10F80000012211461046F9F7D4F961680C2081F8FD +:10F810005000BDE8F081606800F15005042002F05E +:10F8200009F900287DD00E202870012002F0FDFC8F +:10F83000A06861680078C0F3401081F8750000216D +:10F840000520FFF7EDF87048A1684FF0200CC26B5F +:10F850000B78527B23F020030CEA42121A430A7001 +:10F86000C16B95F825304A7B1A404A73C06B28213A +:10F8700080F86610BDE8F081062002F0DBF8002871 +:10F880004FD0614D0F2085F85000022002F0CDFCD2 +:10F890006068012190F880200846F9F78AF9A0688D +:10F8A00061680078C0F3401081F8750001210520DF +:10F8B000FFF7B6F8E86B80F80D80A068017821F0BA +:10F8C00020010170F9F75DFD002818BFFFDF282037 +:10F8D000E96B81F86600BDE8F08122E0052002F0C6 +:10F8E000A9F8F0B101210320FFF79AF8F9F749FDD3 +:10F8F000002818BFFFDF6068012190F880200846CB +:10F90000F9F757F961680D2081F85000BDE8F081E2 +:10F910006068A0F8816080F8836080F85080BDE85E +:10F92000F081BDE8F04100F061B96168032081F821 +:10F930005000BDE8F041082002F077BC606890F804 +:10F940008310490908BF012507D0012908BF0225F6 +:10F9500003D0022914BF00250825C06D00281CBF54 +:10F96000002000F09BF96068806DF9F747F8606847 +:10F9700090F84010022906D0032904BF90F86C10BB +:10F98000012904D010E090F86C1002290CD12A460D +:10F9900015F00C0F04D090F84C00012808BF042289 +:10F9A00001210020F9F705F96068072180F88050EF +:10F9B00080F8616041E000E043E0606890F8831007 +:10F9C000490908BF012507D0012908BF022503D036 +:10F9D000022914BF00250825C06D00281CBF002087 +:10F9E00000F05CF96068806DF9F708F8606890F8DD +:10F9F000401002290AD0032904BF90F86C10012995 +:10FA000008D014E0740100204411002090F86C101C +:10FA100002290CD12A4615F00C0F04D090F84C00A6 +:10FA2000012808BF042201210020F9F7C2F860680C +:10FA3000082180F8805080F8616080F85010BDE89F +:10FA4000F081FFDFBDE8F08170B5FE4C606890F892 +:10FA5000503000210C2B38D001220D2B40D00E2B22 +:10FA600055D00F2B1CBFFFDF70BD042002F0DDFB63 +:10FA7000606890F880100E20F8F7E3FB606890F85B +:10FA8000800010F00C0F14BF282100219620F8F7F9 +:10FA9000D3FFF9F75EF86068052190F88050A06800 +:10FAA000FEF727F8616881F8520048B115F00C0F95 +:10FAB0000CBF50255525F8F714F92846F9F72AF810 +:10FAC00061680B2081F8500070BDF9F742F8002101 +:10FAD0009620F8F7B1FF6168092081F8500070BDE9 +:10FAE00090F88010FF20F8F7ACFB606890F8800079 +:10FAF00010F00C0F14BF282100219620F8F79CFF6E +:10FB0000F9F727F861680A2081F8500070BDA0F865 +:10FB1000811080F8831080F850200020FFF774FDDA +:10FB2000BDE87040032002F080BB70B5C54C606832 +:10FB300090F850007F25801F062828BF70BDDFE8A1 +:10FB400000F0171F1D032A11BE48F9F711F800280D +:10FB500008BF2570F8F7F3FFF8F77CFEBDE87040AA +:10FB6000FEF7D6BEB748F9F703F8C8B9257017E015 +:10FB7000B448F8F7FDFF40B9257006E005F0F6FC43 +:10FB8000B048F8F7F5FF0028F6D0F8F7D8FFBDE841 +:10FB9000704000F02BB8AB48F8F7EAFF0028E5D03A +:10FBA000F8F7CDFF60680021643005F037FEBDE84E +:10FBB000704000F01BB870B5A24C06460D460129F6 +:10FBC00008D0606890F880203046BDE87040134649 +:10FBD00002F077BBF8F75CFA61680346304691F8AB +:10FBE00080202946BDE8704002F06BBB70B5F8F785 +:10FBF00085FFF8F764FFF8F72DFEF8F7D9FE914C72 +:10FC00000025606890F8520030B1F8F78DFFF8F7E2 +:10FC10007CF8606880F852506068022180F85010CB +:10FC2000A0F8815080F88350BDE87040002002F0B9 +:10FC3000FCBA70B5834D06460421A868FEF746F964 +:10FC4000044605F0C8FA002808BF70BD207800F00F +:10FC50003F00252814D2F8F761FA217811F0800FBF +:10FC60000CBF1E214FF49671B4F80120C2F30C02B0 +:10FC700012FB01F10A1AB2F5877F28BF814201D237 +:10FC8000002070BD68682188A0F88110A17880F8F4 +:10FC900083103046BDE8704001F0CCBE2DE9F04144 +:10FCA000684C0746606800F1810690F883004009BF +:10FCB00008BF012507D0012808BF022503D002286C +:10FCC00014BF00250825F8F78DFE307800F03F06B8 +:10FCD0003046F8F7DFFB606880F8736090F86C00DE +:10FCE00002280CBF4020FF202946F8F7AAFA27B1C6 +:10FCF00029460120F8F795FC05E060682A46C16DA9 +:10FD00000120F8F7E2FCF8F724FF0521A068FDF7D1 +:10FD1000F0FE6168002881F8520008BFBDE8F0815C +:10FD200015F00C0F0CBF50245524F7F7DAFF2046CE +:10FD3000BDE8F041F8F7EEBE2DE9F74F414C002544 +:10FD4000914660688A4690F8510000280CBF4FF039 +:10FD500001084FF00008A0680178CE090121FEF7E4 +:10FD6000B5F836B1407900F0C000402808BF012640 +:10FD700000D00026606890F85210002961D090F8F9 +:10FD800040104FF0000B032906D190F839100029DC +:10FD900018BF90F856700ED1A068C17811F03F0FCF +:10FDA0001CBF007910F0010F02D105F061F940B3DA +:10FDB000606890F85370FF2F18BF082F21D0384685 +:10FDC000FDF7D9FB002818BF4FF00108002E38D0EE +:10FDD000606890F8620030B1FDF7F1FD054660689B +:10FDE00080F862B02DE03846FDF791FD054601210F +:10FDF000A068FEF76BF801462846F9F7D3FB0546E5 +:10FE00001FE0F6B1606890F86100D0B9A068C178D1 +:10FE100011F03F0F05D0017911F0010F18BF0B2130 +:10FE200000D105210022FDF7A4FD616881F8520090 +:10FE300038B1FDF7B9FDFF2803D06168012581F8CD +:10FE4000530001E0740100208AF800500098067009 +:10FE500089F8008003B0BDE8F08F2DE9F04FFF4C2A +:10FE600087B00025606890F850002E46801F4FF044 +:10FE70007F08062880F0D581DFE800F00308088BB2 +:10FE8000FDDB00F0F8FB054600F0CCB9F348F8F7CD +:10FE90006FFE002808BF84F80080F8F750FEA068C5 +:10FEA000FDF782FF0546072861D1A068FEF75AF9E1 +:10FEB0000146606890F86C208A4258D190F8501042 +:10FEC000062908BF002005D090F8500008280CBF74 +:10FED0000220012005F08AF970B90321A068FDF71E +:10FEE000F5FF002843D001884078C1F30B010009D9 +:10FEF00005F07BFC00283AD000212846FFF7A1F945 +:10FF0000A0B38DF80C608DF808608DF8046062680D +:10FF1000FF2592F8500008280CBF02210121A0689B +:10FF2000C37813F03F0F1CBF007910F0020F12D0FE +:10FF300092F8800005F0D4F868B901AA03A902A8D4 +:10FF4000FFF7FAFE606890F831509DF80C00002829 +:10FF500018BF45F002052B469DF804209DF80810B7 +:10FF60009DF80C0000F0D5F9054603E0FFE705F029 +:10FF7000FDFA0225606890F85200002800F05281D6 +:10FF8000F8F7D2FDF7F7C1FE606880F8526000F024 +:10FF900049B9A068FDF708FF0646A1686068CA78FD +:10FFA00090F86D309A4221D10A7990F86E309A42D9 +:10FFB0001CD14A7990F86F309A4217D18A7990F81B +:10FFC00070309A4212D1CA7990F871309A420DD1AC +:10FFD0000A7A90F872309A4208D1097890F8740041 +:10FFE000C1F38011814208BF012500D00025F8F738 +:10FFF00031FC9A48F8F7BCFD002808BF84F800805F +:020000040002F8 +:10000000F8F79DFD042E11D185B120787F2808BF17 +:10001000FFDF94F9003084F80080606890F8732066 +:1000200090F87D1090F8540005F06EFB062500F066 +:10003000F9B802278948F8F79BFD002808BF84F823 +:100040000080F8F77CFDA068FDF7AEFE0546A068CD +:10005000FEF788F8082D08BF00287CD1A0684FF073 +:100060000301C27812F03F0F75D0007931EA000029 +:1000700071D1606800E095E000F1500890F8390017 +:10008000002814BF98F8066098F803604FF0000944 +:1000900098F8020078B1FDF787FC0546FF280AD0E2 +:1000A0000146A068401DFDF758FCB5420CBF4FF05B +:1000B00001094FF000090021A068FDF707FF0622A3 +:1000C00008F11D01EEF78CF940B9A068FDF795FE27 +:1000D00098F82410884208BF012000D0002059EA77 +:1000E00000095DD0606800F1320590F831A098F801 +:1000F000010038B13046FDF74BFD00281CBF054616 +:100100004FF0010A4FF00008A06801784FEAD11BB8 +:100110000121FDF7DBFEBBF1000F07D0407900F0B5 +:10012000C000402808BF4FF0010B01D04FF0000B7A +:100130000121A068FDF7CAFE06222946EEF750F914 +:1001400030B9A068FDF766FE504508BF012501D013 +:100150004FF0000500E023E03BEA050018BFFF2E4A +:100160000DD03046FDF7D3FB060008D00121A06872 +:10017000FDF7ACFE01463046F9F714FA804645EA31 +:10018000080019EA000F0BD060680121643005F007 +:1001900045FB01273846FFF737FA052002F045F8FE +:1001A0003D463FE002252D48F8F7E2FC002808BF55 +:1001B00084F80080F8F7C3FCA068FDF7F5FD06465B +:1001C000A068FDF7CFFF072E08BF00282AD1A0683E +:1001D0004FF00101C27812F03F0F23D00279914312 +:1001E00020D1616801F150060021FDF76FFE062263 +:1001F00006F11D01EEF7F4F8A0B9A068FDF7FDFDCA +:1002000096F8241088420DD160680121643005F011 +:1002100005FBFF21022000F009F8002818BF032584 +:1002200000E0FFDF07B02846BDE8F08F2DE9F0437E +:100230000A4C0F4601466068002683B090F87D2086 +:10024000002A35D090F8500008280CBF022501255F +:10025000A168C87810F03F0F02E000007401002090 +:10026000FD484FF000084FF07F0990F900001CBFD7 +:10027000097911F0100F22D07F2808BFFFDF94F911 +:10028000001084F80090606890F85420CDE90021B7 +:10029000029590F8733090F8802000F132013846D2 +:1002A00004F0C0FF05F0AAFA10B305F050F92CE0F5 +:1002B000002914BF0221012180F87D10C2E77F28A8 +:1002C00008BFFFDF94F9001084F80090606890F890 +:1002D0005420CDE90021029590F8733090F88020E9 +:1002E00000F13201384604F09DFF05F030F90CE0D2 +:1002F0000220FFF79EFC30B16068012680F86C8018 +:10030000F8F7A8FA01E005F031F903B03046BDE88E +:10031000F0832DE9F047D04C054684B09A46174645 +:100320000E46A068FDF71EFF4FF00109002800F0FF +:10033000CF804FF00208012808D0022800F00E817B +:1003400005F014F904B04046BDE8F087A068092123 +:10035000C27812F03F0F00F059810279914340F0CA +:100360005581616891F84010032906D012F0020F00 +:1003700008BFFF2118D05DB115E00021FDF7A6FDF3 +:1003800061680622C96B1A31EEF72AF848BB1EE0F5 +:10039000FDF740FD05460121A068FDF797FD2946C0 +:1003A000F7F7FFFF18B15146012000F051B960681E +:1003B00090F84100032818BF022840F02781002E42 +:1003C0001CBFFE21012040F0438100F01FB9A0684E +:1003D000FDF713FD6168C96B497E884208BF01269D +:1003E00000D00026A068C17811F03F0F05D0017938 +:1003F00011F0020F01D06DB338E0616891F842202E +:10040000012A01D096B11BE0D6B90021FDF75EFDAF +:1004100061680268C96BC1F81A208088C883A06827 +:10042000FDF7EBFC6168C96B487609E091F8530071 +:1004300091F85610884203D004B04046BDE8F087DA +:100440006068643005F02EFA002840D004B00F2018 +:10045000BDE8F08767B1FDF7DDFC05460121A06826 +:10046000FDF734FD2946F7F79CFF08B1012200E0B3 +:100470000022616891F84200012807D040B92EB9E6 +:1004800091F8533091F856108B4201D1012100E0D0 +:1004900000210A421BD0012808BF002E11D14FF0C5 +:1004A0000001A068FDF712FD61680268C96BC1F820 +:1004B0001A208088C883A068FDF79FFC6168C96B1B +:1004C00048766068643005F0EDF90028BED19DE003 +:1004D00060682F46554690F840104FF002080329F7 +:1004E000AAD0A168CA7812F03F0F1BBF097911F09A +:1004F000020F002201224FF0FF0A90F85010082945 +:100500000CBF0221012192B190F8800004F0E8FDB7 +:1005100068B95FB9A068FDF77DFC07460121A068B6 +:10052000FDF7D4FC3946F7F73CFF48B1AA465146DF +:100530000020FFF77BFE002818BF4FF003087BE781 +:10054000606890F84100032818BF02287FF474AF58 +:10055000002E18BF4FF0FE0AE9D16DE7616891F8EF +:100560004030032B52D0A0684FF0090CC27812F033 +:100570003F0F4BD002793CEA020C47D1022B06D048 +:1005800012F0020F08BFFF2161D0E5B35EE012F068 +:10059000020F4FF07F0801D04DB114E001F164006B +:1005A00005F080F980B320787F2842D013E067B34C +:1005B000FDF730FC05460121A068FDF787FC2946C0 +:1005C000F7F7EFFE08B36068643005F06BF9D8B157 +:1005D00020787F282DD094F9001084F8008060687E +:1005E00090F85420CDE90021CDF8089090F87330B0 +:1005F00090F8802000F13201504604F013FE0D20E7 +:1006000004B0BDE8F08716E000E001E00220F7E763 +:10061000606890F84100032818BF0228F6D1002E28 +:10062000F4D04FF0FE014FF00200FEF744F9022033 +:10063000E6E7FFDFCFE7FDF7EDFB05460121A06808 +:10064000FDF744FC2946F7F7ACFE38B151460220CD +:10065000FEF731F9DAE7000074010020606890F8D5 +:100660004100032818BF0228D0D1002E1CBFFE2154 +:100670000220EDD1CAE72DE9F84F4FF00008F74806 +:10068000F8F776FA7F27F54C002808BF2770F8F7AF +:1006900056FAA068FDF788FB81460121FEF7D1FDDF +:1006A000616891F88020012A14D0042A1CBF082A0E +:1006B000FFDF00F0D781606890F8520038B1F8F79A +:1006C00033FAF7F722FB6168002081F852004046B8 +:1006D000BDE8F88F0125E24EB9F1080F3AD2DFE804 +:1006E00009F03EC00439393914FC0546F8F7B2F870 +:1006F000002D72D0606890F84000012818BF0228D1 +:100700006BD120787F2869D122E018B391F840009E +:10071000022802D0012818D01CE020787F2808BFCA +:10072000FFDF94F90000277000906068FF2190F8C7 +:10073000733090F85420323004F02FFF61680020AD +:100740004FF00C0881F87D00B5E720787F2860D154 +:10075000FFDF5EE0F8F77EF84FF00608ABE74FF0FA +:100760000008002800F0508191F84000022836D09F +:1007700001284BD003289ED1A068CA6BC37892F899 +:100780001AC0634521D1037992F81BC063451CD17F +:10079000437992F81CC0634517D1837992F81DC044 +:1007A000634512D1C37992F81EC063450DD1037A17 +:1007B00092F81FC0634508D1037892F819C0C3F3BB +:1007C0008013634508BF012300D0002391F8421035 +:1007D00001292CD0C3B300F013B93FE019E0207811 +:1007E0007F2808BFFFDF94F9000027700090606841 +:1007F000FF2190F8733090F85420323004F0CDFE91 +:1008000060684FF00C0880F87D5054E720787F280E +:100810009ED094F90000277000906068FF2190F846 +:10082000733090F85420323004F0B7FE16E0002BFD +:100830007ED102F11A01FDF7C2FAA068FDF7DDFAD8 +:100840006168C96B4876DBE0FFE796F85600082838 +:1008500070D096F8531081426AD0D5E04FF0060868 +:1008600029E7054691F8510000280CBF4FF0010B15 +:100870004FF0000B4FF00008A06810F8092BD209C8 +:1008800007D0407900F0C000402808BF4FF0010AAF +:1008900001D04FF0000A91F84000032806D191F8EA +:1008A0003900002818BF91F8569001D191F8539063 +:1008B0004846FDF72CF80090D8B34846FCF75BFE9D +:1008C000002818BF4FF0010BBAF1000F37D0A06815 +:1008D000A14600F10901009800E0B6E0F8F762FED9 +:1008E0005FEA0008D9F8040090F8319018BF49F089 +:1008F0000209606890F84010032924D0F7F7AAFF96 +:10090000002DABD0F7F75DFD002808BFB8F1000F50 +:100910007DD020787F2808BFFFDF94F90000277082 +:1009200000906068494690F8733090F8542002E0D7 +:1009300066E004E068E0323004F02FFE8EE7606885 +:1009400090F83190D5E7A168C06BCA78837E9A424F +:100950001BD10A79C37E9A4217D14A79037F9A4202 +:1009600013D18A79437F9A420FD1CA79837F9A4201 +:100970000BD10A7AC37F9A4207D10978407EC1F32E +:100980008011814208BF012700D0002796F853004C +:10099000082806D096F85610884208BF4FF0010983 +:1009A00001D04FF00009B8F1000F05D1BBF1000FE5 +:1009B00004D0F7F706FD08B1012000E000204DB19A +:1009C00096F84210012903D021B957EA090101D054 +:1009D000012100E00021084216D0606890F8421022 +:1009E000012908BF002F0BD1C06B00F11A01A068CC +:1009F000FDF7E5F9A068FDF700FA6168C96B487674 +:100A00004FF00E0857E602E0F7F724FF26E760688C +:100A100090F84100032818BF02287FF41FAFBAF1F5 +:100A2000000F3FF41BAF20787F2808BFFFDF94F949 +:100A30000000277000906068FE2190F8733090F8F5 +:100A40005420323004F0A9FD08E791F8481000293D +:100A500018BF00283FF47EAE0BE0000074010020B8 +:100A600044110020B9F1070F7FF474AE00283FF461 +:100A700071AEFEF790FC80461DE60000D0F8001134 +:100A800049B1D0E941231A448B691A448A61D0E9FB +:100A90003F12D16003E0FE4AD0F8FC101162D0E9A9 +:100AA0003F1009B1086170470028FCD00021816126 +:100AB00070472DE9FF4F06460C46488883B040F248 +:100AC000E24148430190E08A002500FB01FA94F8D6 +:100AD0007C0090460D2822D00C2820D024281ED03F +:100AE00094F87D0024281AD000208346069818B177 +:100AF0000121204603F0C0F894F8641094F86500D2 +:100B0000009094F8F0200F464FF47A794AB1012A08 +:100B100061D0022A44D0032A5DD0FFDFB5E0012076 +:100B2000E3E7B8F1000F00D1FFDFD94814F8641FE4 +:100B3000243090F83400F0F7C4FC01902078F8F7E6 +:100B4000CFFB4D4600F2E730B0FBF5F1DFF8409304 +:100B5000D9F80C0001EB00082078F8F7C1FB01463A +:100B600014F86409022816D0012816D040F6340083 +:100B700008444AF2EF010844B0FBF5F10198D9F8B6 +:100B80001C20411A514402EB08000D18012084F882 +:100B9000F0002D1D78E02846EAE74FF4C860E7E74B +:100BA000DFF8EC92A8F10100D9F80810014300D158 +:100BB000FFDFB848B8F1000F016801EB0A0506D065 +:100BC000D9F8080000F22630A84200D9FFDF032040 +:100BD00084F8F00058E094F87C20019D242A05D088 +:100BE00094F87D30242B01D0252A3AD1B4F8702016 +:100BF000B4F81031D21A521C12B2002A31DB94F828 +:100C0000122172B3174694F8132102B110460090D6 +:100C1000022916D0012916D040F6340049F60852B0 +:100C20008118022F12D0012F12D040F63400104448 +:100C3000814210D9081A00F5FA70B0FBF9F00544AA +:100C40000FE04846EAE74FF4C860E7E74846EEE7BA +:100C50004FF4C860EBE7401A00F5FA70B0FBF9F00A +:100C60002D1AB8F1000F0FD0DFF82482D8F8080051 +:100C700018B9B8F8020000B1FFDFD8F8080000F298 +:100C80002630A84200D9FFDF05B9FFDF2946D4F896 +:100C9000F400F4F750FFC4F8F400B06000203070A6 +:100CA0004FF0010886F80480204603F040F8ABF1CD +:100CB0000101084202D186F8058005E094F8F000B1 +:100CC000012844D003207071606A3946009A01F00F +:100CD00042FBF060069830EA0B0035D029463046DA +:100CE000F0F7BAF987B2204603F021F8B8420FD8DE +:100CF000074686F8058005FB07F1D4F8F400F4F701 +:100D00001AFFB06029463046F0F7A6F9384487B29A +:100D10003946204602F0B0FFB068C4F8F400A06E77 +:100D2000002811D0B4F87000B4F89420801A01B2F1 +:100D3000002909DD34F86C0F0144491E91FBF0F1E4 +:100D400089B201FB0020208507B0BDE8F08F0220AA +:100D5000B9E72DE9F04106460C46012001F0DBFA27 +:100D6000C5B20B2001F0D7FAC0B2854200D0FFDF38 +:100D70000025082C7ED2DFE804F00461696965C6AD +:100D80008293304601F0DDFA0621F3F78FF8040074 +:100D900000D1FFDF304601F0D4FA2188884200D02C +:100DA000FFDF94F8F00000B9FFDF204602F00FFEED +:100DB000374E21460020B5607580F561FDF7E9FCEE +:100DC00000F19807606AB84217D994F86500F7F700 +:100DD0000BF9014694F864004FF47A72022828D087 +:100DE000012828D040F6340008444AF2473108442C +:100DF000B0FBF2F1606A0844C51B21460020356152 +:100E0000FDF7C7FC618840F2E24251439830081A6E +:100E1000A0F22630706194F8652094F86410606A3E +:100E200001F099FAA0F5CB70B061BDE8F041F5F79B +:100E300060BE1046D8E74FF4C860D5E7BDE8F04182 +:100E400002F02FBEBDE8F041F7F7D5BE304601F005 +:100E500078FA0621F3F72AF8040000D1FFDF3046C4 +:100E600001F06FFA2188884200D0FFDF01220021C3 +:100E7000204600E047E0BDE8F04101F089BAF7F70D +:100E800073FDF7F7B8FE02204FF0E02104E0000008 +:100E9000CC11002084010020C1F88002BDE8F0815F +:100EA000304601F04EFA0621F3F700F8040000D1B5 +:100EB000FFDF304601F045FA2188884200D0FFDF8D +:100EC00094F8F000042800D0FFDF84F8F05094F884 +:100ED000FA504FF6FF76202D00D3FFDFFA4820F8B6 +:100EE000156094F8FA00F5F720F900B9FFDF20202B +:100EF00084F8FA002046FFF7C1FDF4480078BDE809 +:100F0000F041E2F701B8FFDFC8E770B5EE4C00250D +:100F1000443C84F82850E07868B1E570FEF71EF98B +:100F20002078042803D0606AFFF7A8FD6562E748CF +:100F30000078E1F7E9FFBDE8704001F03ABA70B51A +:100F4000E14C0146443CE069F5F706FE6568A2788D +:100F500090FBF5F172B140F27122B5FBF2F292B260 +:100F6000A36B01FB02F6B34202D901FB123200E08F +:100F70000022A2634D43002800DAFFDF2946E06922 +:100F8000F4F7D9FDE06170BD2DE9F05FFEF736F9A9 +:100F90008246CD48683800F1240881684646D8F872 +:100FA0001800F4F7C8FD0146F069F5F7D5FD4FF0DC +:100FB0000009074686F835903C4640F28F254E469C +:100FC0001EE000BF0AEB06000079F7F70DF80146B6 +:100FD0004AF2B12001444FF47A70B1FBF0F008EB13 +:100FE0008602414692681044844207D3241A91F83D +:100FF0003500A4F28F24401C88F83500761CF6B228 +:1010000098F83600B042DDD8002C10DD98F8351085 +:10101000404608EB81018968A14208D24168C91B9A +:10102000B1F5247F00D30D466C4288F8359098F8CE +:101030003560C3460AEB060898F80400F6F7D4FFBB +:101040004AF2B12101444FF47A7AB1FBFAF298F8EE +:101050000410082909D0042909D0002013180429F4 +:101060000AD0082908D0252207E0082000E0022045 +:1010700000EB40002830F1E70F22521D4FF4A8701A +:10108000082914D0042915D0022916D04FF0080CD5 +:101090005FF0280012FB0C00184462190BEB86036A +:1010A00010449A68D84690420BD8791925E04FF041 +:1010B000400CEFE74FF0100CECE74FF0040C182059 +:1010C000E8E798F8352098F836604046B24210D2EA +:1010D000521C88F835203C1B986862198418084611 +:1010E000F6F782FF4AF2B1210144B1FBFAF001198F +:1010F00003E080F83590D8F80410D8F81C00BDE85B +:10110000F05FF4F718BD2DE9FE4F14460546FEF7D3 +:1011100075F8DFF8B4A10290AAF1440A50469AF893 +:1011200035604FF0000B0AEB86018968CAF83C1065 +:10113000F4B3044600780027042825D005283ED0C3 +:10114000FFDFA04639466069F4F7F5FC0746F5F77E +:101150000BF881463946D8F80440F5F7FDFC401EEF +:1011600090FBF4F0C14361433846F4F7E4FC0146D8 +:10117000C8F81C004846F5F7EFFC002800DDFFDF4B +:10118000012188F813108DE0D4F81490D4F804806D +:1011900001F07AF9070010D0387800B9FFDF7969DB +:1011A00078684A460844414601F05AF9074600E08B +:1011B0000BE04045C5D9FFDFC3E75F46C1E7606A82 +:1011C00001F004F940F6B837BBE7C1690AEB460005 +:1011D0000191408D10B35446DAF81400FFF7AFFECA +:1011E0006168E069F4F7A7FC074684F835B0019C14 +:1011F000D0462046DAF81410F5F7AEFC81463946A1 +:101200002046F5F7A9FCD8F804200146B9FBF2F016 +:10121000B1FBF2F1884242D0012041E0F4F7A4FF93 +:10122000FFF78DFEFFF7B0FE9AF83510DAF804905C +:101230000AEB81010746896800913946DAF81C00FB +:10124000F5F78AFC00248046484504DB98FBF9F456 +:1012500004FB09F41AE0002052469AF8351007E022 +:1012600002EB800304F28F249B68401C1C44C0B234 +:101270008142F5D851B10120F6F7B6FE4AF2B1210C +:1012800001444FF47A70B1FBF0F004440099A8EBEC +:1012900004000C1A00D5FFDFCAF83C40A7E7002085 +:1012A00088F813009AF802005446B8B13946E0694C +:1012B000F5F752FC0146A26B40F2712042438A428C +:1012C00006D2C4F83CB009E03412002080010020AE +:1012D000E06B511A884200D30846E063AF6085F89E +:1012E00000B001202871029F94F835003F1DC05DB9 +:1012F000F6F77AFE4AF23B5101444FF47A70B1FBA3 +:10130000F0F0E16BFE300844E8602078042808D152 +:1013100094F8350004EB4000408D0A2801D20320E8 +:1013200000E00220687104EB4600408DC0B1284601 +:101330006168EFF791FE82B20020761C0CE000BFDE +:1013400004EB4001B0424B8D13449BB24B8501D35B +:101350005B1C4B85401CC0B294F836108142EFD222 +:10136000A8686061A06194F8350004EB4001488DE5 +:10137000401C488594F83500C05D082803D0042837 +:1013800003D000210BE0082100E0022101EB410124 +:1013900028314FF4A872082804D0042802D002286B +:1013A00007D028220A44042805D0082803D0252184 +:1013B00002E01822F6E70F21491D08280CD0042866 +:1013C0000CD002280CD0082011FB0020E16B8842D1 +:1013D00008D20120BDE8FE8F4020F5E71020F3E79A +:1013E0000420F1E70020F5E770B5FE4C061D14F867 +:1013F000352F905DF6F7F8FD4FF47A7100F2E73083 +:10140000B0FBF1F0D4F8071045182078805DF6F7AE +:1014100073FE2178895D082903D0042903D00022B6 +:101420000BE0082200E0022202EB420228324FF4D5 +:10143000A873082904D0042902D0022907D0282340 +:101440001344042905D0082903D0252202E01823DB +:10145000F6E70F22521D08290AD004290AD00229D2 +:101460000AD0082112FB0131081A281A293070BD50 +:101470004021F7E71021F5E70421F3E72DE9FF41CB +:1014800007460C46012000F046FFC5B20B2000F0D5 +:1014900042FFC0B2854200D0FFDF20460126002572 +:1014A000D04C082869D2DFE800F004304646426894 +:1014B0006865667426746078002819D1FDF79EFE71 +:1014C000009594F835108DF808104188C90411D0A2 +:1014D000206C019003208DF80900C24824388560F3 +:1014E000C56125746846FDF768FB002800D0FFDF62 +:1014F000BDE8FF81FFF778FF0190E07C10B18DF827 +:101500000950EAE78DF80960E7E7607840B1207C90 +:1015100008B9FDF7F9FD6574BDE8FF41F4F72FBD8B +:10152000A674FDF739FC0028E2D0FFDFE0E7BDE854 +:10153000FF41F7F760BBFDF761FE4088C00407D0AC +:1015400001210320FDF75EFEA7480078E1F7DCFCEF +:10155000002239466846FFF7D6FD38B1694638465D +:1015600000F0EDFE0028C3D1FFDFC1E7E670FFF712 +:10157000CCFCBDE7BDE8FF41C7E4FFDFB8E7994910 +:1015800050B101228A704A6840F27123B2FBF3F233 +:1015900002EB0010886370470020887070472DE9C7 +:1015A000F05F894640F271218E4E48430025044683 +:1015B000706090462F46D0074AF2B12A4FF47A7BEA +:1015C0000FD0B9F800004843B0600120F6F70CFDD9 +:1015D00000EB0A01B1FBFBF0241AB7680125A4F265 +:1015E0008F245FEA087016D539F8151040F2712083 +:1015F000414306EB85080820C8F80810F6F7F4FC0C +:1016000000EB0A01B1FBFBF0241AD8F80800A4F2A1 +:101610008F2407446D1CA74219D9002D17D0391B00 +:10162000B1FBF5F0B268101AB1FBF5F205FB12122E +:10163000801AB060012008E0B1FBF5F306EB8002F0 +:101640009468E31A401CC0B29360A842F4D3BDE88A +:10165000F09F2DE9F041634C00262078042804D047 +:101660002078052801D00C2018E401206070607CEF +:10167000002538B1EFF3108010F0010F72B610D0D2 +:1016800001270FE0FDF7BAFD074694F82000F5F7B3 +:10169000B2F87888C00411D000210320FDF7B2FD14 +:1016A0000CE00027607C38B1A07C28B1FDF72CFD50 +:1016B0006574A574F4F763FC07B962B694F820006A +:1016C000F5F705FB94F8280030B184F8285020780D +:1016D000052800D0FFDF0C26657000F06AFE30465A +:1016E00012E4404810B5007808B1FFF7B2FF00F0EF +:1016F000D4FE3C4900202439086210BD10B53A4C94 +:1017000058B1012807D0FFDFA06841F66A0188427E +:1017100000D3FFDF10BD40F6C410A060F4E73249EB +:1017200008B508702F4900200870487081F828001B +:10173000C8700874487488742022486281F8202098 +:10174000243948704FF6FF7211F1680121F810201A +:10175000401CC0B22028F9D30020FFF7CFFFFFF7CD +:10176000C0FF1020ADF80000012269460420FFF7F9 +:1017700016FF08BD7FB51B4C05460E46207810B1FC +:101780000C2004B070BD95F8652095F86410686A67 +:1017900000F0C5FEC5F80401656295F8F00000B1DF +:1017A000FFDF104900202439C861052121706070D5 +:1017B00084F82800014604E004EB4102491C5085EE +:1017C000C9B294F836208A42F6D284F83500304601 +:1017D000FFF7D5FE0548F4F74DFC84F820002028DB +:1017E00007D105E0F0110020800100207D140200E7 +:1017F000FFDFF4F7B9FC606194F82010012268461D +:10180000FFF781FC00B9FFDF94F82000694600F083 +:1018100096FD00B9FFDF0020B3E7F94810B5007866 +:1018200008B1002010BD0620F2F7DAFA80F00100BE +:1018300010BDF8B5F24D0446287800B1FFDF002056 +:10184000009023780246DE0701466B4605D060888B +:10185000A188ADF80010012211462678760706D53A +:10186000E088248923F8114042F00802491C491EEF +:1018700085F836101946FFF792FE0020F8BD1FB517 +:1018800011B1112004B010BDDD4C217809B10C203C +:10189000F8E70022627004212170114605E000BFC4 +:1018A00004EB4103491C5A85C9B294F836308B4287 +:1018B000F6D284F83520FFF762FED248F4F7DAFB5F +:1018C00084F82000202800D1FFDF00F0DDFD10B1FA +:1018D000F4F74AFC05E0F4F747FC40F6B831F4F7BA +:1018E0002AF9606194F8201001226846FFF70BFC8A +:1018F00000B9FFDF94F82000694600F020FD00B930 +:10190000FFDF0020BEE770B5BD4C616A0160FFF7E4 +:10191000A0FE050002D1606AFFF7B0F80020606207 +:10192000284670BD7FB5B64C2178052901D00C2022 +:1019300027E7B3492439C860606A00B9FFDF606AED +:1019400090F8F00000B1FFDF606A90F8FA002028FC +:1019500000D0FFDFAC48F4F78DFB616A0546202814 +:1019600081F8FA000E8800D3FFDFA548443020F844 +:101970001560606A90F8FA00202800D1FFDF00238C +:1019800001226846616AFFF794F8606A694690F838 +:10199000FA0000F0D4FC00B9FFDF00206062F0E63E +:1019A000974924394870704710B540F2E24300FB74 +:1019B00003F4002000F0B3FD844201D9201A10BDC9 +:1019C000002010BD70B50D46064601460020FCF70C +:1019D000E0FE044696F86500F6F706FB014696F829 +:1019E00064004FF47A72022815D0012815D040F611 +:1019F000340008444AF247310844B0FBF2F17088E1 +:101A000040F271225043C1EB4000A0F22630A542C3 +:101A100006D2214605E01046EBE74FF4C860E8E740 +:101A20002946814204D2A54201D2204600E0284640 +:101A3000706270BD70B50546FDF7E0FB7049007837 +:101A400024398C689834072D30D2DFE805F004344F +:101A500034252C34340014214FF4A873042810D0FA +:101A60000822082809D02A2102280FD011FB0240A1 +:101A700000222823D118441819E0402211FB02400B +:101A8000F8E7102211FB02402E22F3E7042211FB9B +:101A9000024000221823EDE7282100F04BFC04440B +:101AA00004F5317403E004F5B07400E0FFDF54483E +:101AB000C06BA04201D9012070BD002070BD70B57F +:101AC0004F4C243C607870B1D4E904512846A26898 +:101AD000EFF7EDFA2061A84205D0A169401B084448 +:101AE000A061F5F706F82169A068884201D820783E +:101AF00008B1002070BD012070BD2DE9F04F0546F2 +:101B000085B016460F461C461846F6F7F5FA05EB63 +:101B100047014718204600F0F5FB4AF2C5714FF423 +:101B20007A7908444D46B0FBF5F0384400F160087E +:101B30003348761C24388068304404902046F6F7F9 +:101B4000DBFAA8EB0007204600F0DCFB0646204647 +:101B5000F6F74AFA301AB0FBF5F03A1A18252820A1 +:101B60004FF4C8764FF4BF774FF0020B082C30D0FB +:101B7000042C2BD00021022C2ED0082311F1280197 +:101B800003EB830C0CEB831319440A444FF0000A57 +:101B9000082C29D0042C22D00021022C29D0054663 +:101BA000082001F5B07100BF00EB0010284481420D +:101BB00032D2082C2AD0042C1ED00020022C28D08F +:101BC0000821283001EB0111084434E03946102384 +:101BD000D6E731464023D3E704231831D0E73D460A +:101BE00040F2EE311020DFE735464FF435614020FA +:101BF000DAE70420B431D7E738461021E2E70000E5 +:101C0000F01100207D140200530D020030464021E7 +:101C1000D8E704211830D5E7082C4FD0042C4AD03F +:101C20000021022C4DD0082311F12801C3EBC30081 +:101C300000EB4310084415182821204600F07AFBD9 +:101C400005EB4001082C42D0042C3DD00026022C8C +:101C50003FD0082016F1280600EB801006EB80002C +:101C60000E180120FA4D8DF804008DF800A08DF8B3 +:101C700005B0A86906F22A260499F3F75CFFCDE9BE +:101C800002062046F6F7B0F94AF23B510144B1FB97 +:101C9000F9F0301AFE38E8630298C5F84080A86170 +:101CA00095F82000694600F04AFB002800D1FFDFCC +:101CB00005B0BDE8F08F39461023B7E73146402321 +:101CC000B4E704231831B1E73E461020C4E74020B2 +:101CD000C2E704201836BFE72DE9FE4F06461C4632 +:101CE000174688464FF0010A1846F6F705FAD84D10 +:101CF000243DA9688A1907EB48011144471820467A +:101D000000F000FB4FF47A7BD84600F6FB00B0FBF6 +:101D1000F8F0384400F120092046F6F7EDF9A968FB +:101D20000246A9EB0100801B871A204600F0EAFA60 +:101D300005462046F6F758F9281AB0FBF8F03A1A8B +:101D4000182528204FF4C8774FF4BF78082C2DD0E1 +:101D5000042C28D00021022C2BD0082311F12801BB +:101D600003EB830C0CEB831319440A44082C28D092 +:101D7000042C21D00021022C28D00546082001F592 +:101D8000B07100BF00EB0010284481422AD2082C19 +:101D900022D0042C1DD00020022C20D00821283075 +:101DA00001EB01112CE041461023D9E739464023CD +:101DB000D6E704231831D3E7454640F2EE31102030 +:101DC000E0E73D464FF435614020DBE70420B431C5 +:101DD000D8E740461021E3E738464021E0E70421F8 +:101DE0001830DDE7082C48D0042C43D00020022C0A +:101DF00046D0082110F12800C1EBC10303EB4111CB +:101E0000084415182821204600F094FA05EB4001FB +:101E1000082C3BD0042C36D00027022C38D00820C8 +:101E200017F1280700EB801007EB80000C1804F571 +:101E300096740C98F6F7D8F84AF23B510144B1FB7E +:101E4000FBF0834DFE30A5F12407E96B06F1FE029D +:101E50000844B9680B191A44824224D93219114432 +:101E60000C1AFE342044B0F1807F37D2642C12D299 +:101E7000642011E040461021BEE738464021BBE710 +:101E800004211830B8E747461020CBE74020C9E7C7 +:101E900004201837C6E720460421F4F790FEE8B185 +:101EA000E86B2044E863E0F703FFB9682938314460 +:101EB0000844CDE9000995F835008DF808000220A6 +:101EC0008DF809006846FCF778FE00B1FFDFFCF7EB +:101ED00063FF00B1FFDF5046BDE8FE8F4FF0000A00 +:101EE000F9E71FB500F021FB594C607880B994F8F0 +:101EF000201000226846FFF706F938B194F8200058 +:101F0000694600F01CFA18B9FFDF01E00120E0701B +:101F1000F4F735F800206074A0741FBD2DE9F84F68 +:101F2000FDF76CF90646451CC07840090CD0012825 +:101F30000CD002280CD000202978824608064FF4E5 +:101F4000967407D41E2006E00120F5E70220F3E78F +:101F50000820F1E72046B5F80120C2F30C0212FB7D +:101F600000F7C80901D010B103E01E2401E0FFDF33 +:101F70000024F6F7D3F8A7EB00092878B77909EB26 +:101F80000408C0F3801010B120B1322504E04FF4F2 +:101F9000FA7501E0FFDF00250C2F00D3FFDF2D488D +:101FA0002D4A30F81700291801FB0821501CB1FBFD +:101FB000F0F5F6F76DF8F6F717F84FF47A7100F2CE +:101FC0007160B0FBF1F1A9EB0100471BA7F15900CB +:101FD000103FB0F5247F11D31D4E717829B9024608 +:101FE000534629462046FFF788FD00F09EFAF3F796 +:101FF000C6FF00207074B074BDE8F88F3078009090 +:102000005346224629463846FFF766FE0028F3D19C +:1020100001210220FDF7F6F8BDE8F84F61E710B5A1 +:102020000446012903D10A482438007830B104203D +:1020300084F8F000BDE81040F3F7A1BF00220121B1 +:10204000204600F0A5F934F8700F401C2080F1E71D +:10205000F0110020646302003F420F002DE9F041BF +:102060000746FDF7CBF8050000D1FFDF287810F018 +:102070000C0F01D0012100E00021F74C606A3030E4 +:10208000FCF7C7FA29783846EFF71BFAA4F12406C3 +:102090000146A069B26802446FB32878082803D0CB +:1020A000042803D000230BE0082300E0022303EB05 +:1020B000430328334FF4A877082804D0042802D01B +:1020C000022810D028273B4408280ED004280ED020 +:1020D00002280ED05FF00800C0EBC00707EB4010ED +:1020E0001844983009E01827EDE74020F4E7102065 +:1020F000F2E70420F0E74FF4FC701044471828780A +:102100003F1DF5F771FF014628784FF47A720228D7 +:102110001DD001281DD040F6340008444AF2EF01DA +:102120000844B0FBF2F03A1A606A40F2E241B0466D +:102130004788F0304F43316A81420DD03946206BD9 +:1021400000F08EF90646B84207D9FFDF05E01046D9 +:10215000E3E74FF4C860E0E70026C04880688642A5 +:1021600007D2616A40F271224888424306EB420678 +:1021700004E040F2E240B6FBF0F0616AC882606AB7 +:10218000297880F86410297880F865100521417558 +:10219000C08A6FF41C71484306EB400040F635419D +:1021A000C8F81C00B0EB410F00D3FFDFBDE8F081A1 +:1021B00010B5052937D2DFE801F00509030D31001C +:1021C000002100E00121BDE8104028E7032180F84C +:1021D000F01010BD0446408840F2E24148439F4958 +:1021E000091D0860D4F818010089E082D4F81801AC +:1021F00080796075D4F8180140896080D4F818019E +:102200008089A080D4F81801C089E0802046A16AA6 +:10221000FFF7D8FB022084F8F00010BD816ABDE80A +:102220001040FFF7CFBBFFDF10BD70B58A4C243CD8 +:102230000928A1683FD2DFE800F0050B0B15131544 +:1022400038380800BDE870404BE6BDE8704065E6F0 +:10225000022803D00020BDE87040FFE60120FAE725 +:10226000E16070BD032802D005281CD000E0E160C9 +:102270005FF0000600F059F9774D012085F828003D +:1022800085F83460686AA9690026C0F8F41080F8FF +:10229000F060E068FFF746FB00B1FFDFF3F76FFE89 +:1022A0006E74AE7470BD0126E4E76C480078BDE83A +:1022B0007040E0F729BEFFDF70BD674924394860F0 +:1022C000704770B5644D0446243DB1B14FF47A7641 +:1022D000012903D0022905D0FFDF70BD1846F5F7AC +:1022E000FCFE05E06888401C68801046F6F7F8FFA1 +:1022F00000F2E730B0FBF6F0201AA86070BD564837 +:1023000000787047082803D0042801D0F5F76CBE88 +:102310004EF628307047002804DB00F1E02090F8EA +:10232000000405E000F00F0000F1E02090F8140D2B +:102330004009704710F00C0000D008467047F4F7D1 +:102340003EB910B50446202800D3FFDF4248443090 +:1023500030F8140010BD70B505460C461046F5F770 +:1023600043FE4FF47A71022C0DD0012C0DD040F6B3 +:10237000340210444AF247321044B0FBF1F02844D2 +:1023800000F5CB7070BD0A46F3E74FF4C862F0E782 +:102390001FB513460A46044601466846FEF789FB08 +:1023A00094F8FA006946FFF7CAFF002800D1FFDF62 +:1023B0001FBD70B5284C0025257094F82000F3F758 +:1023C000B4FE00B9FFDF84F8205070BD2DE9F04164 +:1023D000050000D1FFDF204A0024243AD5F804612B +:1023E0002046631E116A08E08869B04203D3984210 +:1023F00001D203460C460846C9680029F4D104B945 +:1024000004460021C5F80041F035C4B1E068E5603C +:10241000E86000B105612E698846A96156B1B069CE +:1024200030B16F69B84200D2FFDFB069C01BA8614C +:10243000C6F81880084D5CB1207820B902E0E96048 +:102440001562E8E7FFDF6169606808442863ADE66C +:10245000C5F83080AAE60000F011002080010020BD +:1024600010B50C4601461046F4F776FB002806DA54 +:10247000211A491EB1FBF4F101FB040010BD90FBD1 +:10248000F4F101FB140010BD2E48016A002001E0A8 +:102490000846C9680029FBD170472DE9FE43294D44 +:1024A0000120287000264FF6FF7420E00621F1F786 +:1024B000FDFC070000D1FFDF97F8FA00F037F4F7D2 +:1024C00006FC07F80A6BA14617F8FA89B8F1200F45 +:1024D00000D3FFDF1B4A683222F8189097F8FA0001 +:1024E000F3F723FE00B9FFDF202087F8FA006946E2 +:1024F0000620F1F764FC50B1FFDF08E0029830B12C +:1025000090F8F01019B10088A042CFD104E06846DD +:10251000F1F733FC0028F1D02E70BDE8FE8310B532 +:10252000FFF719FF00F5C87010BD064800212430E0 +:1025300090F8352000EB4200418503480078E0F731 +:10254000E3BC0000CC11002080010020012804D051 +:10255000022805D0032808D105E0012907D004E0AE +:10256000022904D001E0042901D000207047012095 +:102570007047F748806890F8A21029B1B0F89E1013 +:10258000B0F8A020914215D290F8A61029B1B0F869 +:10259000A410B0F8A02091420CD2B0F89C20B0F862 +:1025A0009A108A4206D290F88020B0F898001AB1AA +:1025B000884203D3012070470628FBD200207047D1 +:1025C0002DE9F041E24D0746A86800F1700490F84B +:1025D000140130B9E27B002301212046EEF7D2FC42 +:1025E00010B1A08D401CA08501263D21AFB92878EF +:1025F000022808D001280AD06878C8B110F0140F5A +:1026000009D01E2039E0162037E026773EE0A86882 +:1026100090F8160131E0020701D56177F5E78107EF +:1026200001D02A2029E0800600D4FFDF232024E007 +:1026300094F8320028B1E08D411CE185218E88425A +:1026400013D294F8360028B1A08E411CA186218EA9 +:1026500088420AD2A18D608D814203D3AA6892F884 +:10266000142112B9228E914201D3222005E0217C4F +:1026700029B1218D814207D308206077C5E7208DDD +:10268000062801D33E20F8E7207FB0B10020207358 +:10269000607320740221A868FFF78AFDA86890F88B +:1026A000E410012904D1D0F81C110878401E0870EC +:1026B000E878BDE8F041E0F727BCA868BDE8F04144 +:1026C0000021FFF775BDA2490C28896881F8E40054 +:1026D00014D0132812D0182810D0002211280ED0A0 +:1026E00007280BD015280AD0012807D0002805D0CC +:1026F000022803D021F89E2F012008717047A1F80D +:10270000A420704710B5924CA1680A88A1F86021F6 +:1027100081F85E0191F8640001F046FBA16881F840 +:10272000620191F8650001F03FFBA16881F8630147 +:10273000012081F85C01002081F82E01E078BDE8DD +:102740001040E0F7E1BB70B5814C00231946A0684A +:1027500090F87C207030EEF715FC00283DD0A06882 +:1027600090F820110025C9B3A1690978B1BB90F890 +:102770007D00EEF7EFFB88BBA168B1F870000A2876 +:102780002DD905220831E069EBF72AFE10B3A068C5 +:10279000D0F81C11087858B10522491CE069EBF704 +:1027A0001FFE002819D1A068D0F81C01007840B99C +:1027B000A068E169D0F81C010A68C0F80120097915 +:1027C0004171A068D0F81C110878401C08700120E5 +:1027D000FFF779FFA06880F8205170BDFFE7A0687F +:1027E00090F8241111B190F82511C1B390F82E1171 +:1027F0000029F2D090F82F110029EED190F87D0039 +:10280000EEF7A8FB0028E8D1A06890F8640001F07A +:10281000CBFA0646A06890F8650001F0C5FA0546B7 +:10282000A06890F830113046FFF790FEA0B3A06882 +:1028300090F831112846FFF789FE68B3A268B2F814 +:10284000703092F86410B2F8320102F58872EEF737 +:1028500001FE20B3A168252081F87C00BDE7FFE7D9 +:1028600090F87D10242918D090F87C10242914D0D9 +:102870005FF0000300F5897200F59271FBF78EFEA0 +:10288000A16881F8245101F13000C28A21F8E62FB5 +:10289000408B4880142007E005E00123EAE7BDE80B +:1028A000704000202EE71620BDE870400BE710B501 +:1028B000F4F7FAFD0C2813D3254C0821A068D0F8B2 +:1028C00018011E30F4F7F4FD28B1A0680421D830B7 +:1028D000F4F7EEFD00B9FFDFBDE810400320F2E69B +:1028E00010BD10B51A4CA068D0F818110A78002A4B +:1028F0001FD04988028891421BD190F87C20002388 +:1029000019467030EEF73EFB002812D0A068D0F8D0 +:1029100018110978022907D003290BD0042919D0EE +:10292000052906D108200DE090F87D00EEF712FB96 +:1029300040B110BD90F8811039B190F8820000B913 +:10294000FFDF0A20BDE81040BDE6BDE81040AEE75D +:102950008C01002090F8AA008007EAD10C20FFF734 +:10296000B2FEA068002120F89E1F01210171017BA9 +:1029700041F00101017310BD70B5F74CA268556EAE +:10298000EEF702FDEBB2C1B200228B4203D0A36886 +:1029900083F8121102E0A16881F81221C5F3072122 +:1029A000C0F30720814203D0A16881F8130114E726 +:1029B000A06880F8132110E710B5E74C0421A06847 +:1029C000FFF7F6FBA06890F85A10012908D000F52F +:1029D0009E71FBF7ACFEE078BDE81040E0F794BADA +:1029E000022180F85A1010BD70B5DB4CA06890F839 +:1029F000E410FE2955D16178002952D190F87F204A +:102A0000002301217030EEF7BDFA002849D1A068FB +:102A100090F8141109B1022037E090F87C200023CF +:102A200019467030EEF7AEFA28B1A06890F896001B +:102A300008B1122029E0A068002590F87C20122A15 +:102A40001DD004DC032A23D0112A04D119E0182A4E +:102A50001AD0232A26D0002304217030EEF792FAF0 +:102A600000281ED1A06890F87D10192971D020DCB3 +:102A700001292AD0022935D0032932D120E00B20A8 +:102A800003E0BDE8704012E70620BDE870401AE69A +:102A900010F8E21F01710720FFF715FEA06880F80B +:102AA0007C509AE61820FFF70EFEA068A0F89E5012 +:102AB00093E61D2918D01E2916D0212966D149E098 +:102AC00010F8E11F4171072070E00C20FFF7FBFDBB +:102AD000A06820F8A45F817941F00101817100F8BC +:102AE000275C53E013202CE090F8252182BB90F85E +:102AF0002421B2B1242912D090F87C1024290ED0C0 +:102B00005FF0000300F5897200F59271FBF746FD56 +:102B1000A0681E2180F87D1080F8245103E0012375 +:102B2000F0E71E2932D1A068FBF797FDFFF744FFBD +:102B3000A16801F13000C28A21F8E62F408B48805D +:102B40001520FFF7C0FDA068A0F8A45080F87D50C4 +:102B50001CE02AE090F8971051B180F8125180F8EB +:102B600013511820FFF7AFFDA068A0F8A4500DE0A6 +:102B700090F82F1151B990F82E1139B1C16DD0F8DC +:102B80003001FFF7F9FE1820FFF79DFDA06890F8CF +:102B9000E400FE2885D1FFF7A4FEA06890F8E400C9 +:102BA000FE2885D1BDE87040CDE51120FFF78BFDF3 +:102BB000A068CBE7684A0129926819D0002302294E +:102BC0000FD003291ED010B301282BD0032807D122 +:102BD00092F87C00132803D0162801D0182804D1BD +:102BE000704792F8E4000028FAD0D2F8180117E0F4 +:102BF00092F8E4000128F3D0D2F81C110878401EA6 +:102C00000870704792F8E4000328EED17047D2F8BC +:102C10001801B2F870108288891A09B20029F5DB10 +:102C200003707047B2F87000B2F82211401A00B277 +:102C30000028F6DBD2F81C010178491E01707047AC +:102C400070B5044690F87C0000250C2810D00D28A3 +:102C50002ED1D4F81811B4F870008988401C88422D +:102C600026D1D4F864013C4E017811B3FFDF42E075 +:102C7000B4F87000B4F82211401C884218D1D4F87E +:102C80001C01D0F80110A160407920730321204677 +:102C9000EDF7F1FDD4F81C01007800B9FFDF012148 +:102CA000FE20FFF787FF84F87C50012084F8B200F3 +:102CB00093E52188C180D4F81801D4F864114089C3 +:102CC0000881D4F81801D4F8641180894881D4F8B7 +:102CD0001801D4F86411C0898881D4F864010571A1 +:102CE000D4F8641109200870D4F864112088488051 +:102CF000F078E0F709F902212046EDF7BCFD032149 +:102D00002046FFF755FAB068D0F81801007802287D +:102D100000D0FFDF0221FE20FFF74CFF84F87C503B +:102D20005BE52DE9F0410C4C00260327D4F808C0E0 +:102D3000012598B12069C0788CF8E20005FA00F00E +:102D4000C0F3C05000B9FFDFA06800F87C7F468464 +:102D500080F82650BDE8F0818C01002000239CF80B +:102D60007D2019460CF17000EEF70CF970B1607817 +:102D70000028EFD12069C178A06880F8E11080F8C0 +:102D80007D70A0F8A46080F8A650E3E76570E1E7E5 +:102D9000F0B5F74C002385B0A068194690F87D2067 +:102DA0007030EEF7EFF8012580B1A06890F87C0054 +:102DB00023280ED024280CD06846F6F785FB68B18E +:102DC000009801A9C0788DF8040008E0657005B08E +:102DD000F0BD607840F020006070F8E70021A06846 +:102DE00003AB162290F87C00EEF74FFB002648B1AB +:102DF000A0689DF80C20162180F80C2180F80D1198 +:102E0000192136E02069FBF722FB78B121690879A6 +:102E100000F00702A06880F85C20497901F0070102 +:102E200080F85D1090F82F310BBB03E00020FFF716 +:102E300078FFCCE790F82E31CBB900F164035F78CE +:102E4000974205D11A788A4202D180F897500EE055 +:102E500000F5AC71028821F8022990F85C200A7113 +:102E600090F85D0048710D70E078E0F74DF8A068CB +:102E7000212180F87D1080F8A650A0F8A460A6E774 +:102E8000F8B5BB4C00231946A06890F87D2070303F +:102E9000EEF778F840B32069FBF7BEFA48B3206933 +:102EA000FBF7B4FA07462069FBF7B4FA0646206937 +:102EB000FBF7AAFA05462069FBF7AAFA0146009734 +:102EC000A06833462A463030FBF79BFBA1680125FA +:102ED00091F87C001C2810D091F85A00012812D0DB +:102EE00091F8250178B90BE0607840F0010060703E +:102EF000F8BDBDE8F840002013E781F85A5002E021 +:102F000091F8240118B11E2081F87D000BE01D20EE +:102F100081F87D0001F5A57231F8300BFBF7FBFB62 +:102F2000E078DFF7F1FFA068002120F8A41F85708A +:102F3000F8BD10B58E4C00230921A06890F87C20C4 +:102F40007030EEF71FF848B16078002805D1A1680D +:102F500001F8960F087301F81A0C10BD012060707B +:102F600010BD7CB5824C00230721A06890F87C201E +:102F70007030EEF707F838B36078002826D169463C +:102F80002069FBF75FFA9DF80000002500F025019D +:102F9000A06880F8B0109DF8011001F0490180F898 +:102FA000B11080F8A250D0F81811008849888142E9 +:102FB00000D0FFDFA068D0F818110D70D0F86411B0 +:102FC0000A7822B1FFDF16E0012060707CBD30F886 +:102FD000E82BCA80C16F0D71C16F009A8A60019A97 +:102FE000CA60C26F0821117030F8E81CC06F4180C0 +:102FF000E078DFF789FFA06880F87C507CBD70B571 +:103000005B4C00231946A06890F87D207030EDF7E6 +:10301000B9FF012540B9A0680023082190F87C2061 +:103020007030EDF7AFFF10B36078002820D1A068B2 +:1030300090F8AA00800712D42069FBF7C9F9A168AB +:1030400081F8AB00206930F8052FA1F8AC2040884A +:10305000A1F8AE0011F8AA0F40F002000870A068B5 +:103060004FF0000690F8AA10C90702D011E0657071 +:103070009DE490F87D20002319467030EDF782FF23 +:1030800000B9FFDFA06880F87D5080F8A650A0F856 +:10309000A460A06890F87C10012906D180F87C60BB +:1030A00080F8A260E078DFF72FFFA168D1F818015F +:1030B000098842888A42DBD101780429D8D1067078 +:1030C000E078DFF721FFA06890F87C100029CFD1CD +:1030D00080F8A2606BE470B5254DA86890F87C106C +:1030E0001A2902D00220687061E469780029FBD1B6 +:1030F000002480F8A74080F8A240D0F8181100887A +:103100004988814200D0FFDFA868D0F818110C7000 +:10311000D0F864110A780AB1FFDF25E090F8A82002 +:1031200072B180F8A8400288CA80D0F864110C718E +:10313000D0F864210E2111700188D0F864010DE0EF +:1031400030F8E82BCA80C16F0C71C26F0121117277 +:10315000C26F0D21117030F8E81CC06F418000F083 +:10316000A1FEE878DFF7D0FEA86880F87C401EE476 +:103170008C01002070B5FA4CA16891F87C20162AC9 +:1031800001D0132A02D191F8A82012B10220607058 +:103190000DE46278002AFBD181F8E000002581F877 +:1031A000A75081F8A250D1F81801098840888842B8 +:1031B00000D0FFDFA068D0F818010078032800D005 +:1031C000FFDF0321FE20FFF7F5FCA068D0F86411B3 +:1031D0000A780AB1FFDF14E030F8E02BCA8010F85B +:1031E000081BC26F1171C16F0D72C26F0D2111707A +:1031F00030F8E81CC06F418000F054FEE078DFF743 +:1032000083FEA06880F87C504BE470B5D44C092153 +:103210000023A06890F87C207030EDF7B3FE002505 +:1032200018B12069007912281ED0A0680A21002355 +:1032300090F87C207030EDF7A5FE18B12069007978 +:10324000142814D02069007916281AD1A06890F8A3 +:103250007C101F2915D180F87C5080F8A250BDE861 +:1032600070401A20FFF74EBABDE8704061E6A068D2 +:1032700000F87C5F458480F82650BDE87040FFF779 +:103280009BBB0EE470B5B64C2079C00773D02069A3 +:1032900000230521C578A06890F87C207030EDF7F8 +:1032A00071FE98B1062D11D006DC022D0ED0042D32 +:1032B0000CD0052D06D109E00B2D07D00D2D05D022 +:1032C000112D03D0607840F008006070607800280D +:1032D00051D12069FAF7E0FF00287ED0206900254F +:1032E0000226C178891E162977D2DFE801F00B7615 +:1032F00034374722764D76254A457676763A5350CE +:103300006A6D7073A0680023012190F87F207030EF +:10331000EDF738FE08BB2069FBF722F8A16881F8B9 +:103320001601072081F87F0081F8A65081F8A2508D +:1033300056E0FFF76AFF53E0A06890F87C100F2971 +:1033400001D066704CE0617839B980F88150122163 +:1033500080F87C1044E000F0D0FD41E000F0ACFDCE +:103360003EE0FBF7A9F803283AD12069FBF7A8F85B +:10337000FFF700FF34E03BE00079F9E7FFF7ABFE31 +:103380002EE0FFF73CFE2BE0FFF7EBFD28E0FFF718 +:10339000D0FD25E0A0680023194690F87D2070300C +:1033A000EDF7F0FD012110B16078C8B901E061705E +:1033B00016E0A06820F8A45F817000F8276C0FE089 +:1033C0000BE0FFF75DFD0BE000F034FD08E0FFF7D8 +:1033D000DFFC05E000F0FAFC02E00020FFF7A1FCB2 +:1033E000A268F2E93001401C41F10001C2E900018C +:1033F0005EE42DE9F0415A4C2079800741D5607890 +:1034000000283ED1E06801270026C178204619290E +:10341000856805F170006FD2DFE801F04B3E0D6F5B +:10342000C1C1801C34C1556287C1C1C1C1BE8B9569 +:1034300098A4B0C1BA0095F87F2000230121EDF7D0 +:10344000A1FD00281DD1A068082180F87F1080F818 +:10345000A26090E0002395F87D201946EDF792FDDB +:1034600010B1A06880F8A660A0680023194690F803 +:103470007C207030EDF786FD002802D0A06880F82F +:10348000A26067E4002395F87C201946EDF77AFDE9 +:1034900000B9FFDF042008E0002395F87C201946DE +:1034A000EDF770FD00B9FFDF0C20A16881F87C000A +:1034B00050E4002395F87C201946EDF763FD00B930 +:1034C000FFDF0D20F1E7002395F87C201946EDF78A +:1034D00059FD00B9FFDFA0680F2180F8A77008E050 +:1034E00095F87C00122800D0FFDFA068112180F839 +:1034F000A87080F87C102DE451E0002395F87C2022 +:103500001946EDF73FFD20B9A06890F8A80000B972 +:10351000FFDFA068132180F8A770EAE795F87C0028 +:10352000182800D0FFDF1A20BFE7BDE8F04100F007 +:1035300063BD002395F87C201946EDF723FD00B903 +:10354000FFDF0520B1E785F8A66003E4002395F8C6 +:103550007C201946EDF716FD00B9FFDF1C20A4E71B +:103560008C010020002395F87D201946EDF70AFD17 +:1035700000B9FFDFA06880F8A66006E4002395F894 +:103580007C201946EDF7FEFC00B9FFDF1F208CE719 +:10359000BDE8F04100F0F8BC85F87D60D3E7FFDFBF +:1035A0006FE710B5F74C6078002837D120794007D5 +:1035B0000FD5A06890F87C00032800D1FFDFA06839 +:1035C00090F87F10072904D101212170002180F893 +:1035D0007F10FFF70EFF00F0B5FCFFF753FEA07859 +:1035E000000716D5A0680023052190F87C207030D4 +:1035F000EDF7C8FC50B108206070A068D0F86411E5 +:1036000008780D2800D10020087002E00020F9F7AA +:10361000F9FCA068BDE81040FFF712BB10BD2DE912 +:10362000F041D84C07464FF00005607808436070C1 +:10363000207981062046806802D5A0F8985004E0E1 +:10364000B0F89810491CA0F8981000F018FD012659 +:10365000F8B1A088000506D5A06890F8821011B1D5 +:10366000A0F88E5015E0A068B0F88E10491CA0F8A4 +:103670008E1000F0F3FCA068B0F88E10B0F8902027 +:10368000914206D3A0F88E5080F83A61E078DFF7D7 +:103690003BFC207910F0600F08D0A06890F88010F3 +:1036A00021B980F880600121FEF782FD1FB9FFF784 +:1036B00078FFFFF799F93846FEF782FFBDE8F04141 +:1036C000F5F711BFAF4A51789378194313D11146DA +:1036D0000128896808D01079400703D591F87F0048 +:1036E000072808D001207047B1F84C00098E8842A5 +:1036F00001D8FEF7E4B900207047A249C278896872 +:10370000012A06D05AB1182A08D1B1F81011FAF7D7 +:10371000BABEB1F822114172090A81727047D1F81C +:10372000181189884173090A8173704770B5954CE7 +:1037300005460E46A0882843A080A80703D5E807C1 +:1037400000D0FFDFE660E80700D02661A80719D5A2 +:10375000F078062802D00B2814D10BE0A06890F86E +:103760007C1018290ED10021E0E93011012100F868 +:103770003E1C07E0A06890F87C10122902D10021BD +:1037800080F88210280601D50820A07068050AD5A7 +:10379000A0688288B0F87010304600F07FFC304698 +:1037A000BDE87040A9E763E43EB505466846F5F715 +:1037B00065FE00B9FFDF222200210098EAF767FECC +:1037C00003210098FAF750FD0098017821F01001CC +:1037D00001702946FAF76DFD6A4C192D71D2DFE8A8 +:1037E00005F020180D3EC8C8C91266C8C9C959C815 +:1037F000C8C8C8BBC9C971718AC89300A1680098BC +:1038000091F8151103E0A168009891F8E610017194 +:10381000B0E0A068D0F81C110098491CFAF795FD9B +:10382000A8E0A1680098D1F8182192790271D1F826 +:10383000182112894271120A8271D1F81821528915 +:10384000C271120A0272D1F8182192894272120AC8 +:103850008272D1F81811C989FAF74EFD8AE0A06882 +:10386000D0F818110098091DFAF77CFDA068D0F86F +:10387000181100980C31FAF77FFDA068D0F81811E4 +:1038800000981E31FAF77EFDA1680098D831FAF74A +:1038900087FD6FE06269009811780171918841712C +:1038A000090A81715188C171090A017262E03649C1 +:1038B000D1E90001CDE9010101A90098FAF78AFDDB +:1038C00058E056E0A068B0F844100098FAF794FD6C +:1038D000A068B0F8E6100098FAF792FDA068B0F87A +:1038E00048100098FAF780FDA068B0F8E81000983A +:1038F000FAF77EFD3EE0A168009891F83021027150 +:1039000091F83111417135E0A06890F81301EDF79D +:1039100032FD01460098FAF7B2FDA06890F8120156 +:1039200000F03DFA70B1A06890F8640000F037FA3A +:1039300040B1A06890F8121190F86400814201D063 +:10394000002002E0A06890F81201EDF714FD014696 +:103950000098FAF790FD0DE0A06890F80D1100981E +:10396000FAF7A8FDA06890F80C110098FAF7A6FDE8 +:1039700000E0FFDFF5F795FD00B9FFDF0098FFF7E6 +:10398000BCFE3EBD8C0100207C63020010B5F94CEA +:10399000A06890F8121109B990F8641080F86410CA +:1039A00090F8131109B990F8651080F8651000209F +:1039B000FEF7A8FEA068FAF750FE002806D0A0681F +:1039C000BDE8104000F59E71FAF7B1BE10BDF8B524 +:1039D000E84E00250446B060B5807570B57035704E +:1039E0000088F5F748FDB0680088F5F76AFDB4F87F +:1039F000F800B168401C82B201F17000EDF75BF88D +:103A000000B1FFDF94F87D00242809D1B4F87010CC +:103A1000B4F81001081A00B2002801DB707830B148 +:103A200094F87C0024280AD0252808D015E0FFF758 +:103A3000ADFF84F87D50B16881F897500DE0B4F87F +:103A40007010B4F81001081A00B2002805DB707875 +:103A500018B9FFF79BFF84F87C50A4F8F850FEF7E4 +:103A600088FD00281CD1B06890F8E400FE2801D041 +:103A7000FFF79AFEC0480090C04BC14A2146284635 +:103A8000F9F7ECF9B0680023052190F87C2070303C +:103A9000EDF778FA002803D0BDE8F840F8F771BFD9 +:103AA000F8BD10B5FEF765FD20B10020BDE810405F +:103AB0000146B4E5BDE81040F9F77FBA70B50C4691 +:103AC000154606464FF4B47200212046EAF7DFFCA3 +:103AD000268005B9FFDF2868C4F818016868C4F8B3 +:103AE0001C01A868C4F8640182E4F0F7E9BA2DE982 +:103AF000F0410D4607460621F0F7D8F9041E3DD0E7 +:103B0000D4F864110026087858B14A8821888A427E +:103B100007D109280FD00E2819D00D2826D0082843 +:103B20003ED094F83A01D0B36E701020287084F81B +:103B30003A61AF809AE06E7009202870D4F8640171 +:103B4000416869608168A9608089A88133E008467E +:103B5000F0F7DDFA0746EFF78AFF70B96E700E20B6 +:103B60002870D4F864014068686011E00846F0F7F6 +:103B7000CEFA0746EFF77BFF08B1002081E46E70B4 +:103B80000D202870D4F8640141686960008928819B +:103B9000D4F8640106703846EFF763FF66E00EE084 +:103BA0006E7008202870D4F86401416869608168EB +:103BB000A960C068E860D4F86401067056E094F823 +:103BC0003C0198B16E70152028700AE084F83C61C1 +:103BD000D4F83E016860D4F84201A860D4F84601E8 +:103BE000E86094F83C010028F0D13FE094F84A01E5 +:103BF00058B16E701C20287084F84A610A2204F5BE +:103C0000A671281DEAF719FC30E094F8560140B17E +:103C10006E701D20287084F85661D4F858016860D1 +:103C200024E094F8340168B16E701A20287004E022 +:103C300084F83461D4F83601686094F834010028BF +:103C4000F6D113E094F85C01002897D06E7016202E +:103C5000287007E084F85C61D4F85E016860B4F80D +:103C60006201288194F85C010028F3D1012008E466 +:103C7000404A5061D170704770B50D4604464EE021 +:103C8000B4F8F800401CA4F8F800B4F89800401C00 +:103C9000A4F89800204600F0F2F9B8B1B4F88E000C +:103CA000401CA4F88E00204600F0D8F9B4F88E002D +:103CB000B4F89010884209D30020A4F88E000120A7 +:103CC00084F83A012B48C078DFF71EF994F8A20077 +:103CD00020B1B4F89E00401CA4F89E0094F8A60001 +:103CE00020B1B4F8A400401CA4F8A40094F8140176 +:103CF00040B994F87F200023012104F17000EDF712 +:103D000041F920B1B4F89C00401CA4F89C00204666 +:103D1000FEF796FFB4F87000401CA4F870006D1E0A +:103D2000ADB2ADD23FE5134AC2E90601704770B5A6 +:103D30000446B0F8980094F88010D1B1B4F89A1005 +:103D40000D1A2D1F94F8960040B194F87C200023A2 +:103D5000092104F17000EDF715F9B8B1B4F88E60DF +:103D6000204600F08CF980B1B4F89000801B001F51 +:103D70000CE007E08C0100201F360200C53602006F +:103D80002D370200C0F10205DCE72846A84200DA20 +:103D90000546002D01DC002005E5A8B203E510F082 +:103DA0000C0000D00120704710B5012808D002286F +:103DB00008D0042808D0082806D0FFDF204610BD10 +:103DC0000124FBE70224F9E70324F7E770B5CC4CA4 +:103DD000A06890F87C001F2804D0607840F00100B3 +:103DE0006070E0E42069FAF73CFBD8B12069012259 +:103DF0000179407901F0070161F30705294600F0D8 +:103E0000070060F30F21A06880F8A2200022A0F82C +:103E10009E20232200F87C2FD0F8B400BDE870402B +:103E2000FEF7AABD0120FEF77CFFBDE870401E2012 +:103E3000FEF768BCF8B5B24C00230A21A06890F8E0 +:103E40007C207030EDF79EF838B32069FAF7E4FA79 +:103E5000C8B12069FAF7DAFA07462069FAF7DAFA00 +:103E600006462069FAF7D0FA05462069FAF7D0FA33 +:103E700001460097A06833462A463030FAF7C1FB66 +:103E8000A068FAF7EAFBA168002081F8A20081F897 +:103E90007C00BDE8F840FEF78FBD607840F001007F +:103EA0006070F8BD964810B580680088F0F72FF96B +:103EB000BDE81040EFF7C6BD10B5914CA36893F86C +:103EC0007C00162802D00220607010BD60780028A7 +:103ED000FBD1D3F81801002200F11E010E30C833C7 +:103EE000ECF7C2FFA0680021C0E92E11012180F883 +:103EF0008110182180F87C1010BD10B5804CA0688E +:103F000090F87C10132902D00220607010BD6178F7 +:103F10000029FBD1D0F8181100884988814200D0CF +:103F2000FFDFA068D0F8181120692631FAF745FAAA +:103F3000A1682069DC31FAF748FAA168162081F8F7 +:103F40007C0010BD10B56E4C207900071BD5607841 +:103F5000002818D1A068002190F8E400FEF72AFE9E +:103F6000A06890F8E400FE2800D1FFDFA068FE21E1 +:103F700080F8E41090F87F10082904D10221217004 +:103F8000002180F87F1010BD70B55D4D2421002404 +:103F9000A86890F87D20212A05D090F87C20232A5B +:103FA00018D0FFDFA0E590F8122112B990F8132184 +:103FB0002AB180F87D10A86880F8A64094E500F842 +:103FC0007D4F847690F8B1000028F4D00020FEF7F1 +:103FD00099FBF0E790F8122112B990F813212AB159 +:103FE00080F87C10A86880F8A2407DE580F87C40CD +:103FF0000020FEF787FBF5E770B5414C0025A0686F +:10400000D0F8181103884A889A4219D109780429EE +:1040100016D190F87C20002319467030ECF7B2FFDF +:1040200000B9FFDFA06890F8AA10890703D4012126 +:1040300080F87C1004E080F8A250D0F818010570D8 +:10404000A0680023194690F87D207030ECF79AFFA5 +:10405000002802D0A06880F8A65045E5B0F890206E +:10406000B0F88E108A4201D3511A00E000218288F4 +:10407000521D8A4202D3012180F89610704710B574 +:1040800090F8821041B990F87C200023062170300E +:10409000ECF778FF002800D0012010BD70B5114466 +:1040A000174D891D8CB2C078A968012806D040B18F +:1040B000182805D191F8120138B109E0A1F8224180 +:1040C00012E5D1F8180184800EE591F8131191B131 +:1040D000FFF765FE80B1A86890F86400FFF75FFE07 +:1040E00050B1A86890F8121190F86420914203D062 +:1040F00090F8130100B90024A868A0F81041F3E477 +:104100008C01002070B58F4C0829207A6CD2DFE832 +:1041100001F004176464276B6B6458B1F4F7D3F8AB +:10412000F5F7FFF80020A072F4F7B4F9BDE870408D +:10413000F4F758BCF5F717F9BDE87040F1F71FBF69 +:10414000DEF7B6FDF5F752F8D4E90001F1F7F3FC1C +:104150002060A07A401CC0B2A072282824D370BD71 +:10416000A07A0025401EC6B2E0683044F4F700FD96 +:1041700010B9E1687F208855A07A272828BF01253B +:10418000DEF796FDA17A01EB4102C2EB81110844F2 +:104190002946F5F755F8A07A282809D2401CC0B264 +:1041A000A072282828BF70BDBDE87040F4F772B92E +:1041B000207A002818BF00F086F8F4F74BFBF4F7DC +:1041C000F7FBF5F7D0F80120E0725F480078DEF7E2 +:1041D0009BFEBDE87040F1F7D2BE002808BF70BD5D +:1041E000BDE8704000F06FB8FFDF70BD10B5554CF2 +:1041F000207A002804BF0C2010BD00202072E0723D +:10420000607AF2F7F8FA607AF2F761FD607AF1F716 +:104210008CFF00280CBF1F20002010BD002270B5AD +:10422000484C06460D46207A68B12272E272607AE6 +:10423000F2F7E1FA607AF2F74AFD607AF1F775FF7A +:10424000002808BFFFDF4048E560067070BD70B50C +:10425000050007D0A5F5E8503C494C3881429CBF89 +:10426000122070BD374CE068002804BF092070BDE3 +:10427000207A00281CBF0C2070BD3548F1F7FAFEEB +:104280006072202804BF1F2070BDF1F76DFF206011 +:104290001DB12946F1F74FFC2060012065602072B6 +:1042A00000F011F8002070BD2649CA7A002A04BF28 +:1042B000002070471E22027000224270CB684360CB +:1042C000CA7201207047F0B585B0F1F74DFF1D4D62 +:1042D0000746394668682C6800EB80004600204697 +:1042E000F2F73AFCB04206DB6868811B3846F1F70A +:1042F00022FC0446286040F2367621463846F2F722 +:104300002BFCB04204DA31463846F1F714FC04467F +:1043100000208DF8000040F6E210039004208DF894 +:10432000050001208DF8040068460294F2F7CAF8EF +:10433000687A6946F2F743F9002808BFFFDF05B045 +:10434000F0BD000074120020AC010020B5EB3C0071 +:10435000054102002DE9F0410C4612490D68114A51 +:10436000114908321160A0F12001312901D3012047 +:104370000CE0412810D040CC0C4F94E80E0007EB25 +:104380008000241F50F8807C3046B84720600548E4 +:10439000001D0560BDE8F081204601F0EBFCF5E76B +:1043A00006207047100502400100000184630200EE +:1043B00010B55548F2F7FAFF00B1FFDF5248401C34 +:1043C000F2F7F4FF002800D0FFDF10BD2DE9F14F18 +:1043D0004E4E82B0D6F800B001274B48F2F7EEFF00 +:1043E000DFF8248120B9002708F10100F2F7FCFF73 +:1043F000474C00254FF0030901206060C4F80051CC +:10440000C4F80451029931602060DFF808A11BE074 +:10441000DAF80000C00617D50E2000F068F8EFF3B8 +:10442000108010F0010072B600D001200090C4F896 +:104430000493D4F8000120B9D4F8040108B901F0BC +:10444000A3FC009800B962B6D4F8000118B9D4F8FA +:1044500004010028DCD0D4F804010028CCD137B105 +:10446000C6F800B008F10100F2F7A8FF11E008F16A +:104470000100F2F7A3FF0028B6D1C4F80893C4F8EE +:104480000451C4F800510E2000F031F81E48F2F734 +:10449000ABFF0020BDE8FE8F2DE9F0438DB00D4647 +:1044A000064600240DF110090DF1200818E000BFA8 +:1044B00004EB4407102255F827106846E9F7BDFFC2 +:1044C00005EB8707102248467968E9F7B6FF68468A +:1044D000FFF77CFF10224146B868E9F7AEFF641C85 +:1044E000B442E5DB0DB00020BDE8F0836EE70028A4 +:1044F00009DB00F01F02012191404009800000F11A +:10450000E020C0F880127047AD01002004E50040B3 +:1045100000E0004010ED00E0B54900200870704751 +:1045200070B5B44D01232B60B34B1C68002CFCD03C +:10453000002407E00E6806601E68002EFCD0001DF7 +:10454000091D641C9442F5D30020286018680028D7 +:10455000FCD070BD70B5A64E0446A84D3078022838 +:1045600000D0FFDFAC4200D3FFDF7169A44801290E +:1045700003D847F23052944201DD03224271491CB4 +:104580007161291BC1609E49707800F02EF90028E6 +:1045900000D1FFDF70BD70B5954C0D466178884243 +:1045A00000D0FFDF954E082D4BD2DFE805F04A041E +:1045B0001E2D4A4A4A382078022800D0FFDF032007 +:1045C0002070A078012801D020B108E0A06801F097 +:1045D00085F904E004F1080007C8FFF7A1FF0520F2 +:1045E0002070BDE87040F1F7CABCF1F7BDFD01468F +:1045F0006068F2F7B1FAB04202D2616902290BD3C6 +:104600000320F2F7FAFD12E0F1F7AEFD0146606813 +:10461000F2F7A2FAB042F3D2BDE870409AE72078F0 +:1046200002280AD0052806D0FFDF04202070BDE84C +:10463000704000F0D0B8022000E00320F2F7DDFD6A +:10464000F3E7FFDF70BD70B50546F1F78DFD684CEF +:1046500060602078012800D0FFDF694901200870E0 +:104660000020087104208D6048716448C8600220F1 +:104670002070607800F0B9F8002800D1FFDF70BD2D +:1046800010B55B4C207838B90220F2F7CCFD18B990 +:104690000320F2F7C8FD08B1112010BD5948F1F709 +:1046A000E9FC6070202804D00120207000206061A7 +:1046B00010BD032010BD2DE9F0471446054600EB60 +:1046C00084000E46A0F1040801F01BF907464FF0E4 +:1046D000805001694F4306EB8401091FB14201D2AA +:1046E000012100E0002189461CB10069B4EB900F64 +:1046F00002D90920BDE8F0872846DCF7A3FD90B970 +:10470000A84510D3BD4205D2B84503D245EA0600FC +:10471000800701D01020EDE73046DCF793FD10B99B +:10472000B9F1000F01D00F20E4E73748374900689E +:10473000884205D0224631462846FFF7F1FE1AE0AE +:10474000FFF79EFF0028D5D1294800218560C0E9E8 +:1047500003648170F2F7D3FD08B12D4801E04AF2FD +:10476000F87060434FF47A7100F2E730B0FBF1F07B +:104770001830FFF768FF0020BCE770B505464FF022 +:10478000805004696C432046DCF75CFD08B10F20C3 +:1047900070BD01F0B6F8A84201D8102070BD1A48CB +:1047A0001A490068884203D0204601F097F810E0CB +:1047B000FFF766FF0028F1D10D4801218460817068 +:1047C000F2F79DFD08B1134800E013481830FFF7D9 +:1047D0003AFF002070BD10B5054C6078F1F7A5FCDC +:1047E00000B9FFDF0020207010BDF1F7E8BE000027 +:1047F000B001002004E5014000E40140105C0C0021 +:1048000084120020974502005C000020BEBAFECA58 +:1048100050280500645E0100A85B01007E4909681C +:104820000160002070477C4908600020704701212A +:104830008A0720B1012804D042F204007047916732 +:1048400000E0D1670020704774490120086042F2FF +:104850000600704708B50423704A1907103230B1BA +:10486000C1F80433106840F0010010600BE01068DC +:1048700020F001001060C1F808330020C1F80801E1 +:10488000674800680090002008BD011F0B2909D867 +:10489000624910310A6822F01E0242EA40000860B4 +:1048A0000020704742F2050070470F2809D85B4985 +:1048B00010310A6822F4706242EA00200860002089 +:1048C000704742F205007047000100F18040C0F8D7 +:1048D000041900207047000100F18040C0F8081959 +:1048E00000207047000100F18040D0F80009086006 +:1048F00000207047012801D907207047494A52F823 +:10490000200002680A43026000207047012801D994 +:1049100007207047434A52F8200002688A43026029 +:1049200000207047012801D9072070473D4A52F8FE +:104930002000006808600020704702003A494FF0EC +:10494000000003D0012A01D0072070470A60704799 +:10495000020036494FF0000003D0012A01D00720A1 +:1049600070470A60704708B54FF40072510510B1E6 +:10497000C1F8042308E0C1F808230020C1F824018D +:1049800027481C3000680090002008BD08B5802230 +:10499000D10510B1C1F8042308E0C1F808230020B4 +:1049A000C1F81C011E48143000680090002008BDAA +:1049B00008B54FF48072910510B1C1F8042308E0E6 +:1049C000C1F808230020C1F82001154818300068FC +:1049D0000090002008BD10493831096801600020AE +:1049E000704770B54FF080450024C5F80841F2F7D4 +:1049F00092FC10B9F2F799FC28B1C5F82441C5F82A +:104A00001C41C5F820414FF0E020802180F80014BF +:104A10000121C0F8001170BD0004004000050040F5 +:104A2000080100404864020078050040800500400D +:104A30006249634B0A6863499A42096801D1C1F32C +:104A400010010160002070475C495D4B0A685D49B8 +:104A5000091D9A4201D1C0F3100008600020704780 +:104A60005649574B0A68574908319A4201D1C0F359 +:104A7000100008600020704730B5504B504D1C6846 +:104A800042F20803AC4202D0142802D203E01128FB +:104A900001D3184630BDC3004B481844C0F8101568 +:104AA000C0F81425002030BD4449454B0A6842F245 +:104AB00009019A4202D0062802D203E0042801D359 +:104AC00008467047404A012142F8301000207047E4 +:104AD0003A493B4B0A6842F209019A4202D0062841 +:104AE00002D203E0042801D308467047364A012168 +:104AF00002EBC00041600020704770B52F4A304E75 +:104B0000314C156842F2090304EB8002B54204D02F +:104B1000062804D2C2F8001807E0042801D318467A +:104B200070BDC1F31000C2F80008002070BD70B560 +:104B3000224A234E244C156842F2090304EB8002FA +:104B4000B54204D0062804D2D2F8000807E00428B1 +:104B500001D3184670BDD2F80008C0F310000860F9 +:104B6000002070BD174910B50831184808601120A1 +:104B7000154A002102EBC003C3F81015C3F8141541 +:104B8000401C1428F6D3002006E0042804D302EBCE +:104B90008003C3F8001807E002EB8003D3F8004855 +:104BA000C4F31004C3F80048401C0628EDD310BD20 +:104BB0000449064808310860704700005C00002086 +:104BC000BEBAFECA00F5014000F001400000FEFF41 +:104BD000814B1B6803B19847BFF34F8F7F48016833 +:104BE0007F4A01F4E06111430160BFF34F8F00BFC2 +:104BF000FDE710B5EFF3108010F0010F72B601D091 +:104C0000012400E0002400F0DDF850B1DCF7BFFB28 +:104C1000F1F755F8F2F793FAF2F7BEFF7149002069 +:104C2000086004B962B6002010BD2DE9F0410C46C1 +:104C30000546EFF3108010F0010F72B601D0012687 +:104C400000E0002600F0BEF820B106B962B60820E8 +:104C5000BDE8F08101F01EF9DCF79DFB0246002063 +:104C600001234709BF0007F1E02700F01F01D7F833 +:104C70000071CF40F9071BD0202803D222FA00F19F +:104C8000C90727D141B2002904DB01F1E02191F8E5 +:104C9000001405E001F00F0101F1E02191F8141D6D +:104CA0004909082916D203FA01F717F0EC0F11D0C1 +:104CB000401C6428D5D3F2F74DFF4B4A4B490020E6 +:104CC000F2F790FF47494A4808602046DCF7C1FAEE +:104CD00060B904E006B962B641F20100B8E73E48A7 +:104CE00004602DB12846DCF701FB18B1102428E040 +:104CF000404D19E02878022802D94FF4805420E072 +:104D000007240028687801D0D8B908E0C8B1202865 +:104D100017D8A878212814D8012812D001E0A87843 +:104D200078B9E8780B280CD8DCF735FB2946F2F780 +:104D3000ECF9F0F783FF00F017FE2846DCF7F4FAF1 +:104D4000044606B962B61CB1FFF753FF20467FE761 +:104D500000207DE710B5044600F034F800B10120D2 +:104D60002070002010BD244908600020704770B5F5 +:104D70000C4622490D682149214E08310E60102849 +:104D800007D011280CD012280FD0132811D00120E1 +:104D900013E0D4E90001FFF748FF354620600DE03D +:104DA000FFF727FF0025206008E02068FFF7D2FF0B +:104DB00003E0114920680860002020600F48001DB2 +:104DC000056070BD07480A490068884201D101208A +:104DD0007047002070470000C80100200CED00E083 +:104DE0000400FA055C0000204814002000000020A8 +:104DF000BEBAFECA50640200040000201005024042 +:104E0000010000017D49C0B20860704700B57C49CF +:104E1000012808BF03200CD0022808BF042008D0B6 +:104E2000042808BF062004D0082816BFFFDF05208D +:104E300000BD086000BD70B505460C46164610461C +:104E4000F3F7D2F8022C08BF4FF47A7105D0012C89 +:104E50000CBF4FF4C86140F6340144183046F3F7F4 +:104E60003CF9204449F6797108444FF47A71B0FB5B +:104E7000F1F0281A70BD70B505460C460846F4F7E7 +:104E80002FFA022C08BF40F24C4105D0012C0CBF78 +:104E900040F634014FF4AF5149F6CA62511A084442 +:104EA0004FF47A7100F2E140B0FBF1F0281A801E55 +:104EB00070BD70B5064615460C460846F4F710FA64 +:104EC000022D08BF4FF47A7105D0012D0CBF4FF4AD +:104ED000C86140F63401022C08BF40F24C4205D0B4 +:104EE000012C0CBF40F634024FF4AF52891A08442B +:104EF00049F6FC6108444FF47A71B0FBF1F0301AC6 +:104F000070BD70B504460E460846F3F76DF80546C9 +:104F10003046F3F7E2F828444AF2AB3108444FF444 +:104F20007A71B0FBF1F0201A801E70BD2DE9F041BE +:104F300007461E460D4614461046082A16BF04288A +:104F40004EF62830F3F750F807EB4701C1EBC711D5 +:104F500000EBC100022D08BF40F24C4105D0012DED +:104F60000CBF40F634014FF4AF5147182846F4F710 +:104F7000B7F9381A4FF47A7100F6B730B0FBF1F593 +:104F80002046F3F7B9F828443044401DBDE8F081CD +:104F900070B5054614460E460846F3F725F805EBAE +:104FA0004502C2EBC512C0EBC2053046F3F795F8D7 +:104FB0002D1A2046082C16BF04284EF62830F3F789 +:104FC00013F828444FF47A7100F6B730B0FBF1F5CE +:104FD0002046F3F791F82844401D70BD0949082880 +:104FE00018BF0428086803BF20F46C5040F4444004 +:104FF00040F0004020F00040086070470C15004071 +:105000001015004040170040F0B585B00C4605462D +:10501000F9F73EF907466E78204603A96A46EEF78F +:1050200002FD81198EB258B1012F02D0032005B0C4 +:10503000F0BD204604AA0399EEF717FC049D01E099 +:10504000022F0FD1ED1C042E0FD32888BDF80010BD +:10505000001D80B2884201D8864202D14FF0000084 +:10506000E5E702D34FF00200E1E74FF00100DEE791 +:10507000FA48C078FF2814BF0120002070472DE9AE +:10508000F041F74C0746160060680D4603D0F9F76B +:1050900069F8A0B121E0F9F765F8D8B96068F9F7C7 +:1050A00061F8D0B915F00C0F17D06068C17811F015 +:1050B0003F0F1CBF007910F0100F0ED00AE0022E37 +:1050C00008D0E6481FB1807DFF2806D002E0C078F6 +:1050D000FF2802D00120BDE8F0810020BDE8F0816A +:1050E0000A4601460120CAE710B5DC4C1D2200210A +:1050F000A01CE9F7CCF97F206077FF202074E070D6 +:10510000A075A08920F060002030A08100202070D0 +:1051100010BD70B5D249486001200870D248D1490D +:10512000002541600570CD4C1D222946A01CE9F7E1 +:10513000AEF97F206077FF202074E070A075A08911 +:1051400020F060002030A081257070BD2DE9F0476F +:10515000C24C06462078C24F4FF0010907F10808FB +:10516000002520B13878D0B998F80000B8B198F887 +:10517000000068B387F80090D8F804103C2239B3D7 +:105180007570301DE9F759F90520307086F80490E4 +:105190003878002818BF88F8005005D015E03D7019 +:1051A000A11C4FF48E72EAE71D220021A01CE9F732 +:1051B0006EF97F206077FF202074E070A075A089D1 +:1051C00020F060002030A08125700120BDE8F0872C +:1051D0000020BDE8F087A148007800280CBF01201E +:1051E000002070470A460146002048E710B510B17C +:1051F000022810D014E09A4C6068F8F7B3FF78B931 +:105200006068C17811F03F0F1CBF007910F0100FDB +:1052100006D1012010BD9148007B10F0080FF8D195 +:10522000002010BD2DE9FF4F81B08C4D8346DDE994 +:105230000F042978DDF838A09846164600291CBFCF +:1052400005B0BDE8F08F8849097800291CBF05B07A +:10525000BDE8F08FE872B4B1012E08BF012708D075 +:10526000022E08BF022704D0042E16BF082E0327E3 +:10527000FFDFEF7385F81E804FF00008784F8CB188 +:10528000022C1DD020E0012E08BF012708D0022EDD +:1052900008BF022704D0042E16BF082E0327FFDF05 +:1052A000AF73E7E77868F8F75DFF68B97868C178A9 +:1052B00011F03F0F1CBF007910F0100F04D110E067 +:1052C000287B10F0080F0CD14FF003017868F8F735 +:1052D000FDFD30B14178090929740088C0F30B0045 +:1052E0006882CDF800807868F8F73CFF0146012815 +:1052F000BDF8000005F102090CBF40F0010020F0EC +:105300000100ADF8000099F80A2012F0020F4ED10A +:10531000022918BF20F0020049D000BFADF80000FC +:1053200010F0020F04D0002908BF40F0080801D097 +:1053300020F00808ADF800807868C17811F03F0FC0 +:105340001CBF007910F0020F0CD0314622464FF0FE +:105350000100FFF794FE002804BF48F00400ADF8F8 +:10536000000006D099F80A00800860F38208ADF8C2 +:10537000008099F80A004109BDF8000061F3461069 +:10538000ADF8000080B20090BDF80000A8810421B3 +:105390007868F8F79BFD002804BFA88920F060001A +:1053A0000CD0B0F80100C004C00C03D007E040F0FE +:1053B0000200B3E7A88920F060004030A8815CB902 +:1053C00016F00C0F08D07868C17811F03F0F1CBFA1 +:1053D000007910F0100F0DD17868C17811F03F0FEF +:1053E00008D0017911F0400F04D00621F8F76EFDC6 +:1053F00000786877314622460020FFF740FE60BB08 +:105400007968C87810F03F0F3FD0087910F0010F8D +:105410003BD0504605F1040905F10308BAF1FF0F2E +:105420000DD04A464146F8F781FA002808BFFFDF51 +:1054300098F8000040F0020088F8000025E00846D7 +:10544000F8F7DBFC88F800007868F8F7ADFC07286F +:105450000CD249467868F8F7B2FC16E094120020A6 +:10546000CC010020D2120020D40100207868F8F787 +:105470009BFC072809D100217868F8F727FD01680F +:10548000C9F800108088A9F804003146224601209E +:10549000FFF7F5FD80BB7868C17811F03F0F2BD086 +:1054A000017911F0020F27D005F1170605F1160852 +:1054B000BBF1020F18BFBBF1030F08D0F8F774FC63 +:1054C00007280AD231467868F8F787FC12E002987C +:1054D000016831608088B0800CE07868F8F764FC7F +:1054E000072807D101217868F8F7F0FC01683160DE +:1054F0008088B08088F800B0002C04BF05B0BDE8FB +:10550000F08F7868F8F72EFE022804BF05B0BDE8DA +:10551000F08F05F11F047868F8F76DFEAB7AC3F1E0 +:10552000FF01884228BF084605D9A98921F06001FA +:1055300001F14001A981C2B203EB04017868F8F7D8 +:1055400062FEA97A0844A87205B0BDE8F08FB048A1 +:105550000178002918BF704701220270007B10F00B +:10556000080F14BF07200620FCF75FBEA848C17BC8 +:10557000002908BF70470122818921F06001403174 +:1055800081810378002B18BF7047027011F0080F5B +:1055900014BF07200620FCF748BE2DE9FF5F9C4F93 +:1055A000DDF838B0914638780E4600281CBF04B0AC +:1055B000BDE8F09FBC1C1D2200212046E8F767FFD4 +:1055C000944D4FF0010A84F800A06868F8F7ECFBEE +:1055D00018B3012826D0022829D0062818BFFFDFDB +:1055E0002AD000BF04F11D016868F8F726FC20727C +:1055F000484604F1020904F10108FF2821D04A4677 +:105600004146F8F793F9002808BFFFDF98F800003B +:1056100040F0020088F8000031E0608940F013009B +:105620006081DFE7608940F015006081E0E7608914 +:1056300040F010006081D5E7608940F01200608181 +:10564000D0E76868F8F7D9FB88F800006868F8F7D1 +:10565000ABFB072804D249466868F8F7B0FB0EE0B8 +:105660006868F8F7A1FB072809D100216868F8F7F6 +:105670002DFC0168C9F800108088A9F8040084F89E +:1056800009B084F80CA000206073FF20A073A17AF9 +:1056900011F0040F08BF20752AD004F1150804F199 +:1056A0001409022E18BF032E09D06868F8F77CFB96 +:1056B00007280CD241466868F8F78FFB16E000987F +:1056C0000168C8F800108088A8F804000EE0686837 +:1056D000F8F76AFB072809D101216868F8F7F6FB9B +:1056E0000168C8F800108088A8F8040089F80060F4 +:1056F0007F20E0760398207787F800A004B006208A +:10570000BDE8F05FFCF791BD2DE9FF5F424F814698 +:105710009A4638788B4600281CBF04B0BDE8F09F3D +:105720003B48017831B1007B10F0100F04BF04B08A +:10573000BDE8F09F1D227C6800212046E8F7A7FE07 +:1057400048464FF00108661C324D84F8008004F191 +:105750000209FF280BD04A463146F8F7E7F800283F +:1057600008BFFFDF307840F0020030701CE068684E +:10577000F8F743FB30706868F8F716FB072804D287 +:1057800049466868F8F71BFB0EE06868F8F70CFB01 +:10579000072809D100216868F8F798FB0168C9F863 +:1057A00000108088A9F8040004F11D016868F8F76A +:1057B00044FB207284F809A060896BF3000040F07C +:1057C0001A00608184F80C8000206073FF20A073B1 +:1057D00020757F20E0760298207787F8008004B05B +:1057E0000720BDE8F05FFCF720BD094A137C834227 +:1057F00005BF508A88420020012070470448007B82 +:10580000C0F3411002280CBF0120002070470000A7 +:1058100094120020CC010020D4010020C2790D2375 +:1058200041B342BB8188012904D94908818004BF62 +:10583000012282800168012918BF002930D0016847 +:105840006FEA0101C1EBC10202EB011281796FEA3B +:10585000010101EB8103C3EB811111444FEA914235 +:1058600001608188B2FBF1F301FB132181714FF0DC +:10587000010102E01AB14FF00001C1717047818847 +:10588000FF2908D24FF6FF7202EA41018180FF2909 +:1058900084BFFF2282800168012918BF0029CED170 +:1058A0000360CCE7817931B1491E11F0FF018171AC +:1058B0001CBF002070470120704710B50121C17145 +:1058C0008171818004460421F1F7E8FD002818BFAA +:1058D00010BD2068401C206010BD00000B4A022152 +:1058E00011600B490B68002BFCD0084B1B1D186086 +:1058F00008680028FCD00020106008680028FCD050 +:1059000070474FF0805040697047000004E5014047 +:1059100000E4014002000B464FF00000014620D099 +:10592000012A04D0022A04D0032A0DD103E0012069 +:1059300002E0022015E00320072B05D2DFE803F088 +:105940000406080A0C0E100007207047012108E029 +:10595000022106E0032104E0042102E0052100E029 +:105960000621F0F7A4BB0000E24805218170002168 +:10597000017041707047E0490A78012A05D0CA6871 +:105980001044C8604038F1F7B4B88A6810448860A1 +:10599000F8E7002819D00378D849D94A13B1012B68 +:1059A0000ED011E00379012B00D06BB943790BB114 +:1059B000012B09D18368643B8B4205D2C0680EE09D +:1059C0000379012B02D00BB10020704743790BB152 +:1059D000012BF9D1C368643B8B42F5D280689042B9 +:1059E000F2D801207047C44901220A70027972B1CD +:1059F00000220A71427962B104224A7182685232ED +:105A00008A60C068C860BB49022088707047032262 +:105A1000EFE70322F1E770B5B74D04460020287088 +:105A2000207988B100202871607978B10420B14EC6 +:105A30006871A168F068F0F77EF8A860E0685230FD +:105A4000E8600320B07070BD0120ECE70320EEE7B2 +:105A50002DE9F04105460226F0F777FF006800B116 +:105A6000FFDFA44C01273DB12878B8B1012805D04B +:105A7000022811D0032814D027710DE06868C828C7 +:105A800008D30421F1F79BF820B16868FFF773FF92 +:105A9000012603E0002601E000F014F93046BDE8DD +:105AA000F08120780028F7D16868FFF772FF00289E +:105AB000E2D06868017879B1A078042800D0FFDFCF +:105AC00001216868FFF7A7FF8B49E07800F003F930 +:105AD0000028E1D1FFDFDFE7FFF785FF6770DBE735 +:105AE0002DE9F041834C0F46E178884200D0FFDF7A +:105AF00000250126082F7DD2DFE807F0040B2828B7 +:105B00003D434F57A0780328C9D00228C7D0FFDFF4 +:105B1000C5E7A078032802D0022800D0FFDF0420C8 +:105B2000A07025712078B8BB0020FFF724FF7248D1 +:105B30000178012906D08068E06000F0EDF820616E +:105B4000002023E0E078F0F734FCF5E7A0780328A4 +:105B500002D0022800D0FFDF207880BB022F08D0BF +:105B60005FF00500F1F749FBA078032840D0A5704D +:105B700095E70420F6E7A078042800D0FFDF022094 +:105B800004E0A078042800D0FFDF0120A168884746 +:105B9000FFF75EFF054633E003E0A078042800D05D +:105BA000FFDFBDE8F04100F08DB8A078042804D0F4 +:105BB000617809B1022800D0FFDF207818B1BDE874 +:105BC000F04100F08AB8207920B10620F1F715FBEA +:105BD00025710DE0607840B14749E07800F07BF82E +:105BE00000B9FFDF65705AE704E00720F1F705FB15 +:105BF000A67054E7FFDF52E73DB1012D03D0FFDF70 +:105C0000022DF9D14BE70420C0E70320BEE770B5B1 +:105C1000050004D0374CA078052806D101E01020FB +:105C200070BD0820F1F7FFFA08B1112070BD3548AA +:105C3000F0F720FAE070202806D00121F1F7DCF817 +:105C40000020A560A07070BD032070BD294810B56C +:105C5000017809B1112010BD8178052906D00129EC +:105C600006D029B101210170002010BD0F2010BD08 +:105C700000F033F8F8E770B51E4C0546A07808B17F +:105C8000012809D155B12846FFF783FE40B1287895 +:105C900040B1A078012809D00F2070BD102070BD40 +:105CA000072070BD2846FFF79EFE03E0002128462E +:105CB000FFF7B1FE1049E07800F00DF800B9FFDF02 +:105CC000002070BD0B4810B5006900F01DF8BDE85C +:105CD0001040F0F754B9F0F772BC064810B5C07820 +:105CE000F0F723FA00B9FFDF0820F1F786FABDE8E4 +:105CF000104039E6DC010020B41300203D8601008D +:105D0000FF1FA107E15A02000C490A6848F202137A +:105D10009A4302430A607047084A116848F2021326 +:105D200001EA03009943116070470246044B1020BA +:105D30001344FC2B01D8116000207047C8060240B4 +:105D40000018FEBF1EF0040F0CBFEFF30880EFF346 +:105D50000980014A10470000FF7B010001B41EB416 +:105D600000B5F1F76DFC01B40198864601BC01B0A5 +:105D70001EBD00008269034981614FF0010010449B +:105D8000704700005D5D02000FF20C0000F10000A2 +:105D9000694641F8080C20BF70470000FEDF184933 +:105DA0000978F9B90420714608421BD10699154AB1 +:105DB000914217DC0699022914DB02394878DF2862 +:105DC00010D10878FE2807D0FF280BD14FF0010032 +:105DD0004FF000020C4B184741F201000099019A64 +:105DE000094B1847094B002B02D01B68DB6818478A +:105DF0004FF0FF3071464FF00002034B1847000090 +:105E000028ED00E000700200D14B020004000020E9 +:105E1000174818497047FFF7FBFFDBF7CFF900BDC4 +:105E2000154816490968884203D1154A13605B6812 +:105E3000184700BD20BFFDE70F4810490968884298 +:105E400010D1104B18684FF0FF318842F2D080F328 +:105E500008884FF02021884204DD0B4802680321A6 +:105E60000A4302600948804709488047FFDF000075 +:105E7000C8130020C81300200010000000000020FC +:105E8000040000200070020014090040B92F000037 +:105E9000215E0200F0B44046494652465B460FB4CC +:105EA00002A0013001B50648004700BF01BC86468C +:105EB0000FBC8046894692469B46F0BC7047000066 +:105EC0000911000004207146084202D0EFF3098155 +:105ED00001E0EFF30881886902380078102813DBAD +:105EE00020280FDB2C280BDB0A4A12680A4B9A4247 +:105EF00003D1602804DB094A10470220086070477C +:105F0000074A1047074A1047074A12682C3212689E +:105F1000104700005C000020BEBAFECA9B130000C0 +:105F2000554302006F4D0200040000200D4B0E4946 +:105F300008470E4B0C4908470D4B0B4908470D4BC2 +:105F4000094908470C4B084908470C4B06490847C4 +:105F50000B4B054908470B4B034908470A4B0249BD +:105F60000847000041BF000079C10000792D000002 +:105F7000F32B0000812B0000012E0000B71300005E +:105F80003F2900007D2F0000455D020000210160D7 +:105F90004160017270470A6802600B7903717047B3 +:105FA00089970000FF9800005B9A0000C59A0000E6 +:105FB000FF9A0000339B0000659B00009D9B000042 +:105FC0003D9C00007D980000859A0000331200007F +:105FD0000744000053440000B94400004745000056 +:105FE0006146000037470000694700004148000053 +:105FF000DB4800002F490000154A0000354A000028 +:10600000AD160000D1160000F11500004D1600007D +:10601000031700009717000003610000C36200002F +:10602000A1660000BB67000043680000C168000073 +:10603000256900004D6A00001D6B0000896B00009F +:10604000574A00005D4A0000674A0000CF4A00003E +:10605000FB4A0000B74C0000E14C0000194D000065 +:10606000834D00006D4E0000834E00007744000019 +:10607000974E0000B94E0000FF4E000033120000A2 +:10608000331200003312000033120000C12500005B +:1060900047260000632600007F2600000D28000030 +:1060A000A9260000B3260000F526000017270000EF +:1060B000F3270000352800003312000033120000DF +:1060C00097840000B7840000B9840000FD840000BC +:1060D0002B8500001B860000A7860000BB86000001 +:1060E000098700001F880000C1890000E98A0000BC +:1060F0003D740000018B00003312000033120000D9 +:10610000EBB700004DB90000A7B9000021BA0000AC +:10611000CDBA0000010000000000000010011001D5 +:106120003A0200001A020000020004050600000006 +:1061300007111102FFFFFFFF0000FFFFF3B3000094 +:10614000273D0000532100008774000001900000EB +:1061500000000000BF9200009B920000AD92000082 +:10616000000002000000000000020000000000002B +:1061700000010000000000004382000023820000B4 +:10618000918200002D250000EF2400000F25000063 +:10619000DBAA000007AB00000FAD0000FD590000B6 +:1061A000B182000000000000E18200007B250000B9 +:1061B000000000000000000000000000F1AB000043 +:1061C00000000000915A00000300000001555555E1 +:1061D000D6BE898E00006606660C661200000A03B1 +:1061E000AE055208000056044608360CC7FD0000F4 +:1061F0005BFF0000A1FB0000C3FD0000A7A8010099 +:106200009B040100AAAED7AB15412010000000008E +:10621000900A0000900A00007B5700007B570000A6 +:10622000E143000053B200000B7700006320000040 +:10623000BD3A020063BD0100BD570000BD5700001C +:1062400005440000E5B2000093770000D72000006D +:10625000EB3A020079BD0100700170014000380086 +:106260005C0024006801200200000300656C746279 +:10627000000000000000000000000000000000001E +:106280008700000000000000000000000000000087 +:10629000BE83605ADB0B376038A5F5AA9183886C02 +:1062A000010000007746010049550100000000018F +:1062B0000206030405000000070000FB349B5F801A +:1062C000000080001000000000000000000000003E +:1062D000060000000A000000320000007300000009 +:1062E000B4000000F401FA00960064004B00320094 +:1062F0001E0014000A000500020001000049000011 +:1063000000000000D7CF0100E9D1010025D1010034 +:10631000EBCF0100000000008FD40100000101025A +:10632000010202030C0802170D0101020909010113 +:1063300006020918180301010909030305000000FA +:10634000555555252627D6BE898E00002BFB01000A +:1063500003F7010049FA01003FF20100BB220200ED +:10636000B7FB0100F401FA00960064004B00320014 +:106370001E0014000A00050002000100254900006B +:1063800000000000314A0200494A0200614A02004E +:10639000794A0200A94A0200D14A0200FB4A0200DF +:1063A0002F4B02007B470200B7460200A1430200C8 +:1063B0002B5D0200AD730100BD730100E9730100A4 +:1063C000BB740100C3740100D57401002F480200A2 +:1063D000494802001D4802002748020055480200B3 +:1063E0008B480200AB480200C9480200D7480200AF +:1063F000E5480200F54802000D4902002549020067 +:106400003B4902005149020000000000DFBC0000CF +:1064100035BD00004BBD000015590200CD43020000 +:10642000994402000F5C02004D5C0200775C0200A0 +:106430009D710100FD760100674902008D4902004F +:10644000B1490200D74902001C0500402005004068 +:10645000001002007464020008000020E80100003F +:106460004411000098640200F0010020D8110000DF +:10647000A011000001181348140244200B440C061C +:106480004813770B1B2034041ABA0401A40213101A +:08649000327F0B744411C000BF +:00000001FF diff --git a/bin/setup-python-for-esp-debug.sh b/bin/setup-python-for-esp-debug.sh new file mode 100644 index 0000000..edba43e --- /dev/null +++ b/bin/setup-python-for-esp-debug.sh @@ -0,0 +1,12 @@ +# shellcheck shell=bash +# (this minor script is actually shell agnostic, and is intended to be sourced rather than run in a subshell) + +# This is a little script you can source if you want to make ESP debugging work on a modern (24.04) ubuntu machine +# It assumes you have built and installed python 2.7 from source with: +# ./configure --enable-optimizations --enable-shared --enable-unicode=ucs4 +# sudo make clean +# make +# sudo make altinstall + +export LD_LIBRARY_PATH=$HOME/packages/python-2.7.18/ +export PYTHON_HOME=/usr/local/lib/python2.7/ diff --git a/bin/test-simulator.sh b/bin/test-simulator.sh new file mode 100644 index 0000000..3c5f8f8 --- /dev/null +++ b/bin/test-simulator.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e + +echo "Starting simulator" +.pio/build/native/program & +sleep 20 # 5 seconds was not enough + +echo "Simulator started, launching python test..." +python3 -c 'from meshtastic.test import testSimulator; testSimulator()' + diff --git a/bin/uf2-convert.bat b/bin/uf2-convert.bat new file mode 100644 index 0000000..242bec3 --- /dev/null +++ b/bin/uf2-convert.bat @@ -0,0 +1,2 @@ +@echo off +if [%1]==[] (echo "Please specify a platformio NRF target (i.e. rak4631) as the first argument.") else (python3 .\bin\uf2conv.py .\.pio\build\%1\firmware.hex -c -o .\.pio\build\%1\firmware.uf2 -f 0xADA52840) \ No newline at end of file diff --git a/bin/uf2conv.py b/bin/uf2conv.py new file mode 100644 index 0000000..a1e241b --- /dev/null +++ b/bin/uf2conv.py @@ -0,0 +1,381 @@ +#!/usr/bin/env python3 +import argparse +import os +import os.path +import re +import struct +import subprocess +import sys + +UF2_MAGIC_START0 = 0x0A324655 # "UF2\n" +UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected +UF2_MAGIC_END = 0x0AB16F30 # Ditto + +families = { + "SAMD21": 0x68ED2B88, + "SAML21": 0x1851780A, + "SAMD51": 0x55114460, + "NRF52": 0x1B57745F, + "STM32F0": 0x647824B6, + "STM32F1": 0x5EE21072, + "STM32F2": 0x5D1A0A2E, + "STM32F3": 0x6B846188, + "STM32F4": 0x57755A57, + "STM32F7": 0x53B80F00, + "STM32G0": 0x300F5633, + "STM32G4": 0x4C71240A, + "STM32H7": 0x6DB66082, + "STM32L0": 0x202E3A91, + "STM32L1": 0x1E1F432D, + "STM32L4": 0x00FF6919, + "STM32L5": 0x04240BDF, + "STM32WB": 0x70D16653, + "STM32WL": 0x21460FF0, + "ATMEGA32": 0x16573617, + "MIMXRT10XX": 0x4FB2D5BD, +} + +INFO_FILE = "/INFO_UF2.TXT" + +appstartaddr = 0x2000 +familyid = 0x0 + + +def is_uf2(buf): + w = struct.unpack(" 476: + assert False, "Invalid UF2 data size at " + ptr + newaddr = hd[3] + if curraddr == None: + appstartaddr = newaddr + curraddr = newaddr + padding = newaddr - curraddr + if padding < 0: + assert False, "Block out of order at " + ptr + if padding > 10 * 1024 * 1024: + assert False, "More than 10M of padding needed at " + ptr + if padding % 4 != 0: + assert False, "Non-word padding size at " + ptr + while padding > 0: + padding -= 4 + outp += b"\x00\x00\x00\x00" + outp += block[32 : 32 + datalen] + curraddr = newaddr + datalen + return outp + + +def convert_to_carray(file_content): + outp = "const unsigned char bindata[] __attribute__((aligned(16))) = {" + for i in range(len(file_content)): + if i % 16 == 0: + outp += "\n" + outp += "0x%02x, " % ord(file_content[i]) + outp += "\n};\n" + return outp + + +def convert_to_uf2(file_content): + global familyid + datapadding = b"" + while len(datapadding) < 512 - 256 - 32 - 4: + datapadding += b"\x00\x00\x00\x00" + numblocks = (len(file_content) + 255) // 256 + outp = b"" + for blockno in range(numblocks): + ptr = 256 * blockno + chunk = file_content[ptr : ptr + 256] + flags = 0x0 + if familyid: + flags |= 0x2000 + hd = struct.pack( + b"= 3 and words[1] == "2" and words[2] == "FAT": + drives.append(words[0]) + else: + rootpath = "/media" + if sys.platform == "darwin": + rootpath = "/Volumes" + elif sys.platform == "linux": + tmp = rootpath + "/" + os.environ["USER"] + if os.path.isdir(tmp): + rootpath = tmp + for d in os.listdir(rootpath): + drives.append(os.path.join(rootpath, d)) + + def has_info(d): + try: + return os.path.isfile(d + INFO_FILE) + except: + return False + + return list(filter(has_info, drives)) + + +def board_id(path): + with open(path + INFO_FILE, mode="r") as file: + file_content = file.read() + return re.search("Board-ID: ([^\r\n]*)", file_content).group(1) + + +def list_drives(): + for d in get_drives(): + print(d, board_id(d)) + + +def write_file(name, buf): + with open(name, "wb") as f: + f.write(buf) + print("Wrote %d bytes to %s" % (len(buf), name)) + + +def main(): + global appstartaddr, familyid + + def error(msg): + print(msg) + sys.exit(1) + + parser = argparse.ArgumentParser(description="Convert to UF2 or flash directly.") + parser.add_argument( + "input", + metavar="INPUT", + type=str, + nargs="?", + help="input file (HEX, BIN or UF2)", + ) + parser.add_argument( + "-b", + "--base", + dest="base", + type=str, + default="0x2000", + help="set base address of application for BIN format (default: 0x2000)", + ) + parser.add_argument( + "-o", + "--output", + metavar="FILE", + dest="output", + type=str, + help='write output to named file; defaults to "flash.uf2" or "flash.bin" where sensible', + ) + parser.add_argument( + "-d", "--device", dest="device_path", help="select a device path to flash" + ) + parser.add_argument( + "-l", "--list", action="store_true", help="list connected devices" + ) + parser.add_argument( + "-c", "--convert", action="store_true", help="do not flash, just convert" + ) + parser.add_argument( + "-D", "--deploy", action="store_true", help="just flash, do not convert" + ) + parser.add_argument( + "-f", + "--family", + dest="family", + type=str, + default="0x0", + help="specify familyID - number or name (default: 0x0)", + ) + parser.add_argument( + "-C", + "--carray", + action="store_true", + help="convert binary file to a C array, not UF2", + ) + args = parser.parse_args() + appstartaddr = int(args.base, 0) + + if args.family.upper() in families: + familyid = families[args.family.upper()] + else: + try: + familyid = int(args.family, 0) + except ValueError: + error( + "Family ID needs to be a number or one of: " + + ", ".join(families.keys()) + ) + + if args.list: + list_drives() + else: + if not args.input: + error("Need input file") + with open(args.input, mode="rb") as f: + inpbuf = f.read() + from_uf2 = is_uf2(inpbuf) + ext = "uf2" + if args.deploy: + outbuf = inpbuf + elif from_uf2: + outbuf = convert_from_uf2(inpbuf) + ext = "bin" + elif is_hex(inpbuf): + outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8")) + elif args.carray: + outbuf = convert_to_carray(inpbuf) + ext = "h" + else: + outbuf = convert_to_uf2(inpbuf) + print( + "Converting to %s, output size: %d, start address: 0x%x" + % (ext, len(outbuf), appstartaddr) + ) + if args.convert or ext != "uf2": + drives = [] + if args.output == None: + args.output = "flash." + ext + else: + drives = get_drives() + + if args.output: + write_file(args.output, outbuf) + else: + if len(drives) == 0: + error("No drive to deploy.") + for d in drives: + print("Flashing %s (%s)" % (d, board_id(d))) + write_file(d + "/NEW.UF2", outbuf) + + +if __name__ == "__main__": + main() diff --git a/bin/update-lilygo_techo_bootloader-0.6.1_nosd.uf2 b/bin/update-lilygo_techo_bootloader-0.6.1_nosd.uf2 new file mode 100644 index 0000000000000000000000000000000000000000..ea293e357d00a1ca32a74624c0b8a057b3eabc45 GIT binary patch literal 75264 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 literal 0 HcmV?d00001 diff --git a/bin/view-map.sh b/bin/view-map.sh new file mode 100644 index 0000000..735dd7c --- /dev/null +++ b/bin/view-map.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +echo using amap tool to display memory map +amap .pio/build/output.map diff --git a/bin/wio_tracker_bootloader_update.bin b/bin/wio_tracker_bootloader_update.bin new file mode 100644 index 0000000..57cd0ad --- /dev/null +++ b/bin/wio_tracker_bootloader_update.bin @@ -0,0 +1,30 @@ +# tips from mark on how to replace the (broken) bootloader on the red wio_tracker_1110 boards. + +~/.platformio/penv/bin/adafruit-nrfutil --verbose dfu serial --package wio_tracker_1110_bootloader-0.9.1_s140_7.3.0.zip -p /dev/ttyACM1 -b 115200 --singlebank --touch 1200 + +exit + +# Output should look like + +Upgrading target on /dev/ttyACM1 with DFU package /home/kevinh/development/meshtastic/WioWM1110/wio_tracker_1110_bootloader-0.9.1_s140_7.3.0.zip. Flow control is disabled, Single bank, Touch 1200 +Touched serial port /dev/ttyACM1 +Opened serial port /dev/ttyACM1 +Starting DFU upgrade of type 3, SoftDevice size: 152728, bootloader size: 39000, application size: 0 +Sending DFU start packet +Sending DFU init packet +Sending firmware file +######################################## +######################################## +######################################## +######################################## +######################################## +######################################## +######################################## +######################################## +######################################## +############### +Activating new firmware + +DFU upgrade took 20.242434978485107s +Device programmed. + diff --git a/boards/CDEBYTE_EoRa-S3.json b/boards/CDEBYTE_EoRa-S3.json new file mode 100644 index 0000000..afaabc5 --- /dev/null +++ b/boards/CDEBYTE_EoRa-S3.json @@ -0,0 +1,38 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld" + }, + "core": "esp32", + "extra_flags": [ + "-D CDEBYTE_EORA_S3", + "-D ARDUINO_USB_CDC_ON_BOOT=1", + "-D ARDUINO_USB_MODE=0", + "-D ARDUINO_RUNNING_CORE=1", + "-D ARDUINO_EVENT_RUNNING_CORE=1", + "-D BOARD_HAS_PSRAM" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "dio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "CDEBYTE_EoRa-S3" + }, + "connectivity": ["wifi", "bluetooth"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "CDEBYTE EoRa-S3", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 327680, + "maximum_size": 4194304, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://www.cdebyte.com/Module-Testkits-EoRaPI", + "vendor": "CDEBYTE" +} diff --git a/boards/ESP32-S3-WROOM-1-N4.json b/boards/ESP32-S3-WROOM-1-N4.json new file mode 100644 index 0000000..160926b --- /dev/null +++ b/boards/ESP32-S3-WROOM-1-N4.json @@ -0,0 +1,39 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld" + }, + "core": "esp32", + "extra_flags": [ + "-D ARDUINO_USB_CDC_ON_BOOT=0", + "-D ARDUINO_USB_MSC_ON_BOOT=0", + "-D ARDUINO_USB_DFU_ON_BOOT=0", + "-D ARDUINO_USB_MODE=0", + "-D ARDUINO_RUNNING_CORE=1", + "-D ARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "ESP32-S3-WROOM-1-N4" + }, + "connectivity": ["wifi", "bluetooth"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "ESP32-S3-WROOM-1-N4 (4 MB Flash, No PSRAM)", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 524288, + "maximum_size": 4194304, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://www.espressif.com/sites/default/files/documentation/esp32-s3-wroom-1_wroom-1u_datasheet_en.pdf", + "vendor": "Espressif" +} diff --git a/boards/bpi_picow_esp32_s3.json b/boards/bpi_picow_esp32_s3.json new file mode 100644 index 0000000..75983d8 --- /dev/null +++ b/boards/bpi_picow_esp32_s3.json @@ -0,0 +1,38 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld" + }, + "core": "esp32", + "extra_flags": [ + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1", + "-DBOARD_HAS_PSRAM" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "dio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "bpi_picow_esp32_s3" + }, + "connectivity": ["wifi"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "BPI-PicoW-S3 (8 MB FLASH, 2 MB PSRAM)", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 327680, + "maximum_size": 8388608, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://wiki.banana-pi.org/BPI-PicoW-S3", + "vendor": "BPI" +} diff --git a/boards/canaryone.json b/boards/canaryone.json new file mode 100644 index 0000000..f64a4a7 --- /dev/null +++ b/boards/canaryone.json @@ -0,0 +1,52 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_CANARY -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x4405"], + ["0x239A", "0x009F"] + ], + "usb_product": "CanaryOne", + "mcu": "nrf52840", + "variant": "canaryone", + "variants_dir": "variants", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": ["jlink"], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "Canary (Adafruit BSP)", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://canaryradio.io/", + "vendor": "Canary Radio Company" +} diff --git a/boards/eink0.1.json b/boards/eink0.1.json new file mode 100644 index 0000000..cb636ea --- /dev/null +++ b/boards/eink0.1.json @@ -0,0 +1,47 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_TTGO_EINK -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [["0x239A", "0x4405"]], + "usb_product": "TTGO_eink", + "mcu": "nrf52840", + "variant": "eink0.1", + "variants_dir": "variants", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": ["jlink"], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "TTGO eink (Adafruit BSP)", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "require_upload_port": true, + "speed": 115200, + "protocol": "jlink", + "protocols": ["jlink", "nrfjprog", "stlink"] + }, + "url": "FIXME", + "vendor": "TTGO" +} diff --git a/boards/esp32-s3-pico.json b/boards/esp32-s3-pico.json new file mode 100644 index 0000000..8f8c6fd --- /dev/null +++ b/boards/esp32-s3-pico.json @@ -0,0 +1,40 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_16MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DARDUINO_ESP32S3_DEV", + "-DARDUINO_USB_MODE=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "Waveshare ESP32-S3-Pico (16 MB FLASH, 2 MB PSRAM)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://www.waveshare.com/esp32-s3-pico.htm", + "vendor": "Waveshare" +} diff --git a/boards/heltec_mesh_node_t114.json b/boards/heltec_mesh_node_t114.json new file mode 100644 index 0000000..2bd306e --- /dev/null +++ b/boards/heltec_mesh_node_t114.json @@ -0,0 +1,53 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DHELTEC_T114 -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x4405"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"] + ], + "usb_product": "HT-n5262", + "mcu": "nrf52840", + "variant": "heltec_mesh_node_t114", + "variants_dir": "variants", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": ["jlink"], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "Heltec nrf (Adafruit BSP)", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "FIXME", + "vendor": "Heltec" +} diff --git a/boards/heltec_vision_master_e213.json b/boards/heltec_vision_master_e213.json new file mode 100644 index 0000000..bf5fe15 --- /dev/null +++ b/boards/heltec_vision_master_e213.json @@ -0,0 +1,42 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_8MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [ + ["0x303A", "0x1001"], + ["0x303A", "0x0002"] + ], + "mcu": "esp32s3", + "variant": "heltec_vision_master_e213" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "Heltec Vision Master E213", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 327680, + "maximum_size": 8388608, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://heltec.org/project/vision-master-e213/", + "vendor": "Heltec" +} diff --git a/boards/heltec_vision_master_e290.json b/boards/heltec_vision_master_e290.json new file mode 100644 index 0000000..70f7d5f --- /dev/null +++ b/boards/heltec_vision_master_e290.json @@ -0,0 +1,42 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_8MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [ + ["0x303A", "0x1001"], + ["0x303A", "0x0002"] + ], + "mcu": "esp32s3", + "variant": "heltec_vision_master_e290" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "Heltec Vision Master E290", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 327680, + "maximum_size": 8388608, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://heltec.org/project/vision-master-e290/", + "vendor": "Heltec" +} diff --git a/boards/heltec_vision_master_t190.json b/boards/heltec_vision_master_t190.json new file mode 100644 index 0000000..341e702 --- /dev/null +++ b/boards/heltec_vision_master_t190.json @@ -0,0 +1,42 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_8MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [ + ["0x303A", "0x1001"], + ["0x303A", "0x0002"] + ], + "mcu": "esp32s3", + "variant": "heltec_vision_master_t190" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "Heltec Vision Master t190", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 327680, + "maximum_size": 8388608, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://heltec.org/project/vision-master-t190/", + "vendor": "Heltec" +} diff --git a/boards/heltec_wireless_tracker.json b/boards/heltec_wireless_tracker.json new file mode 100644 index 0000000..04c6e55 --- /dev/null +++ b/boards/heltec_wireless_tracker.json @@ -0,0 +1,38 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_8MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DHELTEC_WIRELESS_TRACKER", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "heltec_wireless_tracker" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "Heltec Wireless Tracker", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 327680, + "maximum_size": 8388608, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://heltec.org/project/wireless-tracker/", + "vendor": "Heltec" +} diff --git a/boards/icarus.json b/boards/icarus.json new file mode 100644 index 0000000..03da468 --- /dev/null +++ b/boards/icarus.json @@ -0,0 +1,41 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=0" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x2886", "0x0059"]], + "mcu": "esp32s3", + "variant": "icarus" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "icarus", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 8388608, + "maximum_size": 8388608, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://icarus.azlan.works", + "vendor": "Muhammad Shah" +} diff --git a/boards/me25ls01-4y10td.json b/boards/me25ls01-4y10td.json new file mode 100644 index 0000000..9e1d632 --- /dev/null +++ b/boards/me25ls01-4y10td.json @@ -0,0 +1,58 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DME25LS01_4Y10TD -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"] + ], + "usb_product": "ME25LS01-BOOT", + "mcu": "nrf52840", + "variant": "MINEWSEMI_ME25LS01", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd" + }, + "frameworks": ["arduino"], + "name": "Minesemi ME25LS01", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink", + "cmsis-dap", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://en.minewsemi.com/lora-module/lr1110-nrf52840-me25LS01l", + "vendor": "Minesemi" +} diff --git a/boards/ms24sf1.json b/boards/ms24sf1.json new file mode 100644 index 0000000..8356e30 --- /dev/null +++ b/boards/ms24sf1.json @@ -0,0 +1,58 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DMS24SF1 -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"] + ], + "usb_product": "MS24SF1-BOOT", + "mcu": "nrf52840", + "variant": "MINEWSEMI_MS24SF1_SX1262", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd" + }, + "frameworks": ["arduino"], + "name": "MINEWSEMI_MS24SF1_SX1262", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink", + "cmsis-dap", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://en.minewsemi.com/lora-module/nrf52840-sx1262-ms24sf1", + "vendor": "Minesemi" +} diff --git a/boards/my-esp32s3-diy-oled.json b/boards/my-esp32s3-diy-oled.json new file mode 100644 index 0000000..69a24be --- /dev/null +++ b/boards/my-esp32s3-diy-oled.json @@ -0,0 +1,41 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=0" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "my-esp32s3-diy-oled" + }, + "connectivity": ["wifi"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "Clone ESP32-S3-DevKitC-1 v1.1 (16 MB FLASH, 8 MB PSRAM)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/hw-reference/esp32s3/user-guide-devkitc-1.html", + "vendor": "Espressif" +} diff --git a/boards/my_esp32s3_diy_eink.json b/boards/my_esp32s3_diy_eink.json new file mode 100644 index 0000000..432424f --- /dev/null +++ b/boards/my_esp32s3_diy_eink.json @@ -0,0 +1,41 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=0" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "my_esp32s3_diy_eink" + }, + "connectivity": ["wifi"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "Clone ESP32-S3-DevKitC-1 v1.1 (16 MB FLASH, 8 MB PSRAM)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/hw-reference/esp32s3/user-guide-devkitc-1.html", + "vendor": "Espressif" +} diff --git a/boards/nano-g2-ultra.json b/boards/nano-g2-ultra.json new file mode 100644 index 0000000..7afce17 --- /dev/null +++ b/boards/nano-g2-ultra.json @@ -0,0 +1,51 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"] + ], + "usb_product": "BQ nRF52840", + "mcu": "nrf52840", + "variant": "nano-g2-ultra", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd" + }, + "frameworks": ["arduino"], + "name": "BQ nRF52840", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://wiki.uniteng.com/en/meshtastic/nano-g2-ultra", + "vendor": "BQ Consulting" +} diff --git a/boards/nordic_pca10059.json b/boards/nordic_pca10059.json new file mode 100644 index 0000000..6540817 --- /dev/null +++ b/boards/nordic_pca10059.json @@ -0,0 +1,52 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DNORDIC_PCA10059 -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"] + ], + "usb_product": "PCA10059", + "mcu": "nrf52840", + "variant": "nRF52840 Dongle", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "nRF52840 Dongle", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://www.nordicsemi.com/Products/Development-hardware/nrf52840-dongle", + "vendor": "Nordic Semiconductor" +} diff --git a/boards/nrf52840_dk.json b/boards/nrf52840_dk.json new file mode 100644 index 0000000..8a16e85 --- /dev/null +++ b/boards/nrf52840_dk.json @@ -0,0 +1,47 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_PCA10056 -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [["0x239A", "0x4404"]], + "usb_product": "nrf52840dk", + "mcu": "nrf52840", + "variant": "pca10056", + "variants_dir": "variants", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": ["jlink"], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "A modified NRF52840-DK devboard (Adafruit BSP)", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "require_upload_port": true, + "speed": 115200, + "protocol": "jlink", + "protocols": ["jlink", "nrfjprog", "stlink"] + }, + "url": "https://meshtastic.org/", + "vendor": "Nordic Semi" +} diff --git a/boards/promicro-nrf52840.json b/boards/promicro-nrf52840.json new file mode 100644 index 0000000..99ae3f0 --- /dev/null +++ b/boards/promicro-nrf52840.json @@ -0,0 +1,52 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x00B3"], + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"] + ], + "usb_product": "ProMicro compatible nRF52840", + "mcu": "nrf52840", + "variant": "promicro_diy", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd" + }, + "frameworks": ["arduino"], + "name": "ProMicro compatible nRF52840", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["nrfutil", "jlink", "nrfjprog", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://www.nologo.tech/product/otherboard/NRF52840.html", + "vendor": "Nologo" +} diff --git a/boards/seeed-sensecap-indicator.json b/boards/seeed-sensecap-indicator.json new file mode 100644 index 0000000..3fc5712 --- /dev/null +++ b/boards/seeed-sensecap-indicator.json @@ -0,0 +1,42 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_8MB.csv", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=0", + "-DARDUINO_USB_MODE=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x1A86", "0x7523"]], + "mcu": "esp32s3", + "variant": "esp32s3r8" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino"], + "name": "Seeed Studio SenseCAP Indicator", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 327680, + "maximum_size": 8388608, + "require_upload_port": true, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "speed": 921600 + }, + "url": "https://www.seeedstudio.com/Indicator-for-Meshtastic.html", + "vendor": "Seeed Studio" +} diff --git a/boards/seeed-xiao-s3.json b/boards/seeed-xiao-s3.json new file mode 100644 index 0000000..0b7b432 --- /dev/null +++ b/boards/seeed-xiao-s3.json @@ -0,0 +1,41 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=0" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x2886", "0x0059"]], + "mcu": "esp32s3", + "variant": "seeed-xiao-s3" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "seeed-xiao-s3", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 8388608, + "maximum_size": 8388608, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://www.seeedstudio.com/XIAO-ESP32S3-Sense-p-5639.html", + "vendor": "Seeed Studio" +} diff --git a/boards/station-g2.json b/boards/station-g2.json new file mode 100644 index 0000000..871f067 --- /dev/null +++ b/boards/station-g2.json @@ -0,0 +1,41 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=0" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "station-g2" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "BQ Station G2", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://wiki.uniteng.com/en/meshtastic/station-g2", + "vendor": "BQ Consulting" +} diff --git a/boards/t-deck.json b/boards/t-deck.json new file mode 100644 index 0000000..d62ec48 --- /dev/null +++ b/boards/t-deck.json @@ -0,0 +1,41 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=0" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "t-deck" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "Espressif Systems LilyGO T-Deck (16 MB FLASH, 8 MB PSRAM)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://www.lilygo.cc/en-pl/products/t-deck", + "vendor": "LilyGO" +} diff --git a/boards/t-echo.json b/boards/t-echo.json new file mode 100644 index 0000000..fcfc8c5 --- /dev/null +++ b/boards/t-echo.json @@ -0,0 +1,53 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_TTGO_EINK -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x4405"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"] + ], + "usb_product": "TTGO_eink", + "mcu": "nrf52840", + "variant": "t-echo", + "variants_dir": "variants", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": ["jlink"], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "TTGO eink (Adafruit BSP)", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "FIXME", + "vendor": "TTGO" +} diff --git a/boards/t-watch-s3.json b/boards/t-watch-s3.json new file mode 100644 index 0000000..e6e3633 --- /dev/null +++ b/boards/t-watch-s3.json @@ -0,0 +1,43 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DT_WATCH_S3", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [ + ["0x303A", "0x1001"], + ["0x303A", "0x0002"] + ], + "mcu": "esp32s3", + "variant": "t-watch-s3" + }, + "connectivity": ["wifi"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino"], + "name": "LilyGo T-Watch 2020 V3", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 327680, + "maximum_size": 8388608, + "require_upload_port": true, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "speed": 921600 + }, + "url": "https://www.lilygo.cc/en-pl/products/t-watch-s3", + "vendor": "LilyGo" +} diff --git a/boards/tbeam-s3-core.json b/boards/tbeam-s3-core.json new file mode 100644 index 0000000..7bda2e5 --- /dev/null +++ b/boards/tbeam-s3-core.json @@ -0,0 +1,37 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DLILYGO_TBEAM_S3_CORE", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "dio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "tbeam-s3-core" + }, + "connectivity": ["wifi"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino"], + "name": "LilyGo TBeam-S3-Core", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 327680, + "maximum_size": 8388608, + "require_upload_port": true, + "speed": 921600 + }, + "url": "http://www.lilygo.cn/", + "vendor": "LilyGo" +} diff --git a/boards/tlora-t3s3-v1.json b/boards/tlora-t3s3-v1.json new file mode 100644 index 0000000..0bfd17a --- /dev/null +++ b/boards/tlora-t3s3-v1.json @@ -0,0 +1,38 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld" + }, + "core": "esp32", + "extra_flags": [ + "-DLILYGO_T3S3_V1", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1", + "-DBOARD_HAS_PSRAM" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "dio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "tlora-t3s3-v1" + }, + "connectivity": ["wifi", "bluetooth"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "LilyGo TLora-T3S3-V1", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 327680, + "maximum_size": 4194304, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "http://www.lilygo.cn/", + "vendor": "LilyGo" +} diff --git a/boards/tracker-t1000-e.json b/boards/tracker-t1000-e.json new file mode 100644 index 0000000..2be716e --- /dev/null +++ b/boards/tracker-t1000-e.json @@ -0,0 +1,58 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_WIO_WM1110 -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"] + ], + "usb_product": "T1000-E-BOOT", + "mcu": "nrf52840", + "variant": "Seeed_T1000-E", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd" + }, + "frameworks": ["arduino"], + "name": "Seeed T1000-E", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink", + "cmsis-dap", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://www.seeedstudio.com/SenseCAP-Card-Tracker-T1000-E-for-Meshtastic-p-5913.html", + "vendor": "Seeed Studio" +} diff --git a/boards/unphone.json b/boards/unphone.json new file mode 100644 index 0000000..bf71199 --- /dev/null +++ b/boards/unphone.json @@ -0,0 +1,46 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi", + "partitions": "default_8MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DUNPHONE_SPIN=9", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [ + ["0x16D0", "0x1178"], + ["0x303a", "0x1001"] + ], + "mcu": "esp32s3", + "variant": "unphone" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "unPhone", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 327680, + "maximum_size": 8323072, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://unphone.net/", + "vendor": "University of Sheffield" +} diff --git a/boards/wio-sdk-wm1110.json b/boards/wio-sdk-wm1110.json new file mode 100644 index 0000000..f45b030 --- /dev/null +++ b/boards/wio-sdk-wm1110.json @@ -0,0 +1,51 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_WIO_WM1110 -DNRF52840_XXAA", + "f_cpu": "64000000L", + "mcu": "nrf52840", + "variant": "Seeed_WIO_WM1110", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd" + }, + "frameworks": ["arduino", "freertos"], + "name": "Seeed WIO WM1110", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink", + "cmsis-dap", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://www.seeedstudio.com/Wio-WM1110-Dev-Kit-p-5677.html", + "vendor": "Seeed Studio" +} diff --git a/boards/wio-t1000-s.json b/boards/wio-t1000-s.json new file mode 100644 index 0000000..654a8f7 --- /dev/null +++ b/boards/wio-t1000-s.json @@ -0,0 +1,58 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_WIO_WM1110 -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"] + ], + "usb_product": "WIO-BOOT", + "mcu": "nrf52840", + "variant": "Seeed_WIO_WM1110", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd" + }, + "frameworks": ["arduino"], + "name": "Seeed WIO WM1110", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink", + "cmsis-dap", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://www.seeedstudio.com/LoRaWAN-Tracker-c-1938.html", + "vendor": "Seeed Studio" +} diff --git a/boards/wio-tracker-wm1110.json b/boards/wio-tracker-wm1110.json new file mode 100644 index 0000000..37a9186 --- /dev/null +++ b/boards/wio-tracker-wm1110.json @@ -0,0 +1,58 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_WIO_WM1110 -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"] + ], + "usb_product": "WIO-BOOT", + "mcu": "nrf52840", + "variant": "Seeed_WIO_WM1110", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd" + }, + "frameworks": ["arduino"], + "name": "Seeed WIO WM1110", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink", + "cmsis-dap", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://www.seeedstudio.com/Wio-Tracker-1110-Dev-Board-p-5799.html", + "vendor": "Seeed Studio" +} diff --git a/boards/wiphone.json b/boards/wiphone.json new file mode 100644 index 0000000..bb01f42 --- /dev/null +++ b/boards/wiphone.json @@ -0,0 +1,34 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32_out.ld", + "partitions": "default_16MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DARDUINO_WIPHONE14", + "-DBOARD_HAS_PSRAM", + "-mfix-esp32-psram-cache-issue", + "-mfix-esp32-psram-cache-strategy=memw" + ], + "f_cpu": "240000000L", + "f_flash": "40000000L", + "flash_mode": "dio", + "mcu": "esp32", + "variant": "wiphone", + "board": "WiPhone" + }, + "connectivity": ["wifi", "bluetooth"], + "frameworks": ["arduino", "espidf"], + "name": "WIPhone Integrated 1.4", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 532480, + "maximum_size": 6553600, + "maximum_data_size": 4521984, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://www.wiphone.io/", + "vendor": "HackEDA" +} diff --git a/boards/wiscore_rak11200.json b/boards/wiscore_rak11200.json new file mode 100644 index 0000000..54ee9b6 --- /dev/null +++ b/boards/wiscore_rak11200.json @@ -0,0 +1,27 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32_out.ld" + }, + "core": "esp32", + "extra_flags": ["-DBOARD_HAS_PSRAM", "-DARDUINO_ESP32_DEV"], + "f_cpu": "240000000L", + "f_flash": "40000000L", + "flash_mode": "dio", + "mcu": "esp32", + "variant": "WisCore_RAK11200_Board" + }, + "connectivity": ["wifi", "bluetooth", "ethernet", "can"], + "frameworks": ["arduino", "espidf"], + "name": "WisCore RAK11200 Board", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 327680, + "maximum_size": 4194304, + "protocols": ["esptool", "espota", "ftdi"], + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://www.rakwireless.com", + "vendor": "RAKwireless" +} diff --git a/boards/wiscore_rak11300.json b/boards/wiscore_rak11300.json new file mode 100644 index 0000000..19beee7 --- /dev/null +++ b/boards/wiscore_rak11300.json @@ -0,0 +1,40 @@ +{ + "build": { + "arduino": { + "earlephilhower": { + "boot2_source": "boot2_w25q080_2_padded_checksum.S", + "usb_vid": "0x2E8A", + "usb_pid": "0x000A" + } + }, + "core": "earlephilhower", + "cpu": "cortex-m0plus", + "extra_flags": "-DARDUINO_GENERIC_RP2040 -DRASPBERRY_PI_PICO -DARDUINO_ARCH_RP2040 -DUSBD_MAX_POWER_MA=250", + "f_cpu": "133000000L", + "hwids": [ + ["0x2E8A", "0x00C0"], + ["0x2E8A", "0x000A"] + ], + "mcu": "rp2040", + "variant": "WisBlock_RAK11300_Board" + }, + "debug": { + "jlink_device": "RP2040_M0_0", + "openocd_target": "rp2040.cfg", + "svd_path": "rp2040.svd" + }, + "frameworks": ["arduino"], + "name": "WisBlock RAK11300", + "upload": { + "maximum_ram_size": 270336, + "maximum_size": 2097152, + "require_upload_port": true, + "native_usb": true, + "use_1200bps_touch": true, + "wait_for_upload_port": false, + "protocol": "picotool", + "protocols": ["cmsis-dap", "raspberrypi-swd", "picotool", "picoprobe"] + }, + "url": "https://docs.rakwireless.com/", + "vendor": "RAKwireless" +} diff --git a/boards/wiscore_rak3172.json b/boards/wiscore_rak3172.json new file mode 100644 index 0000000..714e091 --- /dev/null +++ b/boards/wiscore_rak3172.json @@ -0,0 +1,30 @@ +{ + "build": { + "arduino": { + "variant_h": "variant_RAK3172_MODULE.h" + }, + "core": "stm32", + "cpu": "cortex-m4", + "extra_flags": "-DSTM32WLxx -DSTM32WLE5xx -DARDUINO_GENERIC_WLE5CCUX", + "f_cpu": "48000000L", + "mcu": "stm32wle5ccu", + "variant": "STM32WLxx/WL54CCU_WL55CCU_WLE4C(8-B-C)U_WLE5C(8-B-C)U", + "product_line": "STM32WLE5xx" + }, + "debug": { + "default_tools": ["stlink"], + "jlink_device": "STM32WLE5CC", + "openocd_target": "stm32wlx", + "svd_path": "STM32WLE5_CM4.svd" + }, + "frameworks": ["arduino"], + "name": "BB-STM32WL", + "upload": { + "maximum_ram_size": 65536, + "maximum_size": 262144, + "protocol": "cmsis-dap", + "protocols": ["cmsis-dap", "stlink"] + }, + "url": "https://www.st.com/en/microcontrollers-microprocessors/stm32wl-series.html", + "vendor": "ST" +} diff --git a/boards/wiscore_rak4600.json b/boards/wiscore_rak4600.json new file mode 100644 index 0000000..0dec90a --- /dev/null +++ b/boards/wiscore_rak4600.json @@ -0,0 +1,50 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52832_s132_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DNRF52832_XXAA -DNRF52", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"] + ], + "usb_product": "Feather nRF52832 Express", + "mcu": "nrf52832", + "variant": "WisCore_RAK4600_Board", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS132", + "sd_name": "s132", + "sd_version": "6.1.1", + "sd_fwid": "0x00B7" + }, + "zephyr": { + "variant": "nrf52_adafruit_feather" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52832_xxAA", + "svd_path": "nrf52.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino", "zephyr"], + "name": "Adafruit Bluefruit nRF52832 Feather", + "upload": { + "maximum_ram_size": 65536, + "maximum_size": 524288, + "require_upload_port": true, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"] + }, + "url": "https://www.adafruit.com/product/3406", + "vendor": "Adafruit" +} diff --git a/boards/wiscore_rak4631.json b/boards/wiscore_rak4631.json new file mode 100644 index 0000000..c783f33 --- /dev/null +++ b/boards/wiscore_rak4631.json @@ -0,0 +1,52 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"] + ], + "usb_product": "WisCore RAK4631 Board", + "mcu": "nrf52840", + "variant": "WisCore_RAK4631_Board", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino", "freertos"], + "name": "WisCore RAK4631 Board", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://www.rakwireless.com", + "vendor": "RAKwireless" +} diff --git a/boards/xiao_ble_sense.json b/boards/xiao_ble_sense.json new file mode 100644 index 0000000..8e0212b --- /dev/null +++ b/boards/xiao_ble_sense.json @@ -0,0 +1,58 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_MDBT50Q_RX -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x810B"], + ["0x239A", "0x010B"], + ["0x239A", "0x810C"] + ], + "usb_product": "XIAO-BOOT", + "mcu": "nrf52840", + "variant": "Seeed_XIAO_nRF52840_Sense", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "Seeed Xiao BLE Sense", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink", + "cmsis-dap", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://www.seeedstudio.com/Seeed-XIAO-BLE-Sense-nRF52840-p-5253.html", + "vendor": "Seeed Studio" +} diff --git a/data/static/.gitkeep b/data/static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..82f2647 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: "3.7" + +services: + meshtastic-node: + build: . + deploy: + mode: replicated + replicas: 4 + networks: + - mesh + +networks: + mesh: diff --git a/extra_scripts/README.md b/extra_scripts/README.md new file mode 100644 index 0000000..4c797f4 --- /dev/null +++ b/extra_scripts/README.md @@ -0,0 +1,3 @@ +# extra_scripts + +This directory contains special [scripts](https://docs.platformio.org/en/latest/scripting/index.html) that are used to modify the platformio environment in rare cases. diff --git a/extra_scripts/disable_adafruit_usb.py b/extra_scripts/disable_adafruit_usb.py new file mode 100644 index 0000000..5962421 --- /dev/null +++ b/extra_scripts/disable_adafruit_usb.py @@ -0,0 +1,28 @@ +# trunk-ignore-all(flake8/F821) +# trunk-ignore-all(ruff/F821) + +Import("env") + +# NOTE: This is not currently used, but can serve as an example on how to write extra_scripts + +# print("Current CLI targets", COMMAND_LINE_TARGETS) +# print("Current Build targets", BUILD_TARGETS) +# print("CPP defs", env.get("CPPDEFINES")) +# print(env.Dump()) + +# Adafruit.py in the platformio build tree is a bit naive and always enables their USB stack for building. We don't want this. +# So come in after that python script has run and disable it. This hack avoids us having to fork that big project and send in a PR +# which might not be accepted. -@geeksville + +lib_builders = env.get("__PIO_LIB_BUILDERS", None) +if lib_builders is not None: + print("Disabling Adafruit USB stack") + for k in lib_builders: + if k.name == "Adafruit TinyUSB Library": + libenv = k.env + # print(f"{k.name }: { libenv.Dump() } ") + # libenv["CPPDEFINES"].remove("USBCON") + libenv["CPPDEFINES"].remove("USE_TINYUSB") + +# Custom actions when building program/firmware +# env.AddPreAction("buildprog", callback...) diff --git a/images/compass.png b/images/compass.png new file mode 100644 index 0000000000000000000000000000000000000000..c4e5b589bc42cd7b05148dbbdee01ce0fe689548 GIT binary patch literal 594 zcmV-Y0D z5=T6yMwv0j@Tjp$r>9Aaf?4_RvEr{NF-VEc*cngsTjXN9KczyNS{PNkjF4(sU2O#uRdw#WqKsWgv;Y7A07*qoM6N<$f(pF`asU7T literal 0 HcmV?d00001 diff --git a/images/face-24px.svg b/images/face-24px.svg new file mode 100644 index 0000000..08e8f5f --- /dev/null +++ b/images/face-24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/face.png b/images/face.png new file mode 100644 index 0000000000000000000000000000000000000000..eb6b91e13b89f3a0d27efbaf83e35f2c1f70b3c5 GIT binary patch literal 225 zcmeAS@N?(olHy`uVBq!ia0vp@Ak4u6ByT*@`3|Hw3p^r=85r3AgD_)kZiXOGaI&Y1 zV@SoV)(f_LjDaF;50?jc_w;s1bDGX|N@L|(+oCG5S#gP|rk^ws;8E!2TD-wUOu6?JX z+cOTY1qO$2NeF8%Id|aNz5SDUC#=wM)#jINSWq;9E6AO{c9F}oMJ3xVX5M+n` \ No newline at end of file diff --git a/images/pin.png b/images/pin.png new file mode 100644 index 0000000000000000000000000000000000000000..3c91a726c3cf3c5605e1580a89e35f84850da2f6 GIT binary patch literal 203 zcmeAS@N?(olHy`uVBq!ia0vp@Ak4u6ByT*@`3|Hw3p^r=85r3AgD_)kZiXOGu-4PX zF{I*F?*&7?gA5XF57QGh3nj{QVjNXe9;rNH(NlPLi1*Hdh$Dx*et9p+T>SFFbLsnU zFCV#b^5{jEqfu&gr`KqF-P3Z#Ps^rM^q7-Gl1j_OLk=9=I;|JNGpafA)8!wobIsK5 x%Kh_l$J2*efrjTV`}8(%2-n^?-K6Rrx8fB;x5m_RU7$l4JYD@<);T3K0RZxvO;-Q_ literal 0 HcmV?d00001 diff --git a/images/room-24px.svg b/images/room-24px.svg new file mode 100644 index 0000000..48e55ab --- /dev/null +++ b/images/room-24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/textsms-24px.svg b/images/textsms-24px.svg new file mode 100644 index 0000000..84c4fdc --- /dev/null +++ b/images/textsms-24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/monitor/filter_c3_exception_decoder.py b/monitor/filter_c3_exception_decoder.py new file mode 100644 index 0000000..6d7b537 --- /dev/null +++ b/monitor/filter_c3_exception_decoder.py @@ -0,0 +1,148 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re +import subprocess +import sys + +from platformio.project.exception import PlatformioException +from platformio.public import DeviceMonitorFilterBase, load_build_metadata + +# By design, __init__ is called inside miniterm and we can't pass context to it. +# pylint: disable=attribute-defined-outside-init + +IS_WINDOWS = sys.platform.startswith("win") + + +class Esp32C3ExceptionDecoder(DeviceMonitorFilterBase): + NAME = "esp32_c3_exception_decoder" + + PCADDR_PATTERN = re.compile(r"0x4[0-9a-f]{7}", re.IGNORECASE) + + def __call__(self): + self.buffer = "" + self.pcaddr_re = self.PCADDR_PATTERN + + self.firmware_path = None + self.addr2line_path = None + self.enabled = self.setup_paths() + + if self.config.get("env:" + self.environment, "build_type") != "debug": + print( + """ +Please build project in debug configuration to get more details about an exception. +See https://docs.platformio.org/page/projectconf/build_configurations.html + +""" + ) + + return self + + def setup_paths(self): + self.project_dir = os.path.abspath(self.project_dir) + try: + data = load_build_metadata(self.project_dir, self.environment) + self.firmware_path = data["prog_path"] + if not os.path.isfile(self.firmware_path): + sys.stderr.write( + "%s: disabling, firmware at %s does not exist, rebuild the project?\n" + % (self.__class__.__name__, self.firmware_path) + ) + return False + + if self.addr2line_path is None: + cc_path = data.get("cc_path", "") + if "-gcc" in cc_path: + self.addr2line_path = cc_path.replace("-gcc", "-addr2line") + else: + sys.stderr.write( + "%s: disabling, failed to find addr2line.\n" + % self.__class__.__name__ + ) + return False + + if not os.path.isfile(self.addr2line_path): + sys.stderr.write( + "%s: disabling, addr2line at %s does not exist\n" + % (self.__class__.__name__, self.addr2line_path) + ) + return False + + return True + except PlatformioException as e: + sys.stderr.write( + "%s: disabling, exception while looking for addr2line: %s\n" + % (self.__class__.__name__, e) + ) + return False + + def rx(self, text): + if not self.enabled: + return text + + last = 0 + while True: + idx = text.find("\n", last) + if idx == -1: + if len(self.buffer) < 4096: + self.buffer += text[last:] + break + + line = text[last:idx] + if self.buffer: + line = self.buffer + line + self.buffer = "" + last = idx + 1 + + # Output each trace on a separate line below ours + # Logic identical to https://github.com/espressif/esp-idf/blob/master/tools/idf_monitor_base/logger.py#L131 + for m in re.finditer(self.pcaddr_re, line): + if m is None: + continue + + trace = self.get_backtrace(m) + if len(trace) != "": + text = text[:last] + trace + text[last:] + last += len(trace) + + return text + + def get_backtrace(self, match): + trace = "\n" + enc = "mbcs" if IS_WINDOWS else "utf-8" + args = [self.addr2line_path, "-fipC", "-e", self.firmware_path] + try: + addr = match.group() + output = subprocess.check_output(args + [addr]).decode(enc).strip() + output = output.replace( + "\n", "\n " + ) # newlines happen with inlined methods + output = self.strip_project_dir(output) + # Output the trace in yellow color so that it is easier to spot + trace += "\033[33m=> %s: %s\033[0m\n" % (addr, output) + except subprocess.CalledProcessError as e: + sys.stderr.write( + "%s: failed to call %s: %s\n" + % (self.__class__.__name__, self.addr2line_path, e) + ) + return trace + + def strip_project_dir(self, trace): + while True: + idx = trace.find(self.project_dir) + if idx == -1: + break + trace = trace[:idx] + trace[idx + len(self.project_dir) + 1 :] + return trace diff --git a/partition-table.csv b/partition-table.csv new file mode 100644 index 0000000..e0d44cf --- /dev/null +++ b/partition-table.csv @@ -0,0 +1,8 @@ +# FIXME! using the genpartitions based table doesn't work on TTGO so for now I stay with my old memory map +# This is a layout for 4MB of flash +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x009000, 0x005000, +otadata, data, ota, 0x00e000, 0x002000, +app, app, ota_0, 0x010000, 0x250000, +flashApp, app, ota_1, 0x260000, 0x0A0000, +spiffs, data, spiffs, 0x300000, 0x100000, \ No newline at end of file diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..bbb4287 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,166 @@ +; PlatformIO Project Configuration File +; https://docs.platformio.org/page/projectconf.html + +[platformio] +default_envs = native +;default_envs = pico +;default_envs = tbeam-s3-core +;default_envs = tbeam0.7 +;default_envs = heltec-v1 +;default_envs = heltec-v2_0 +;default_envs = heltec-v2_1 +;default_envs = heltec-wireless-tracker +;default_envs = chatter2 +;default_envs = tlora-v1 +;default_envs = tlora_v1_3 +;default_envs = tlora-v2 +;default_envs = tlora-v2-1-1_6 +;default_envs = tlora-v2-1-1_6-tcxo +;default_envs = tlora-t3s3-v1 +;default_envs = t-echo +;default_envs = canaryone +;default_envs = native +;default_envs = nano-g1 +;default_envs = pca10059_diy_eink +;default_envs = meshtastic-diy-v1 +;default_envs = meshtastic-diy-v1_1 +;default_envs = meshtastic-dr-dev +;default_envs = m5stack-coreink +;default_envs = rak4631 +;default_envs = rak4631_eth_gw +;default_envs = rak2560 +;default_envs = rak10701 +;default_envs = wio-e5 +;default_envs = radiomaster_900_bandit_nano +;default_envs = radiomaster_900_bandit_micro +;default_envs = radiomaster_900_bandit +;default_envs = heltec_capsule_sensor_v3 +;default_envs = heltec_vision_master_t190 +;default_envs = heltec_vision_master_e213 +;default_envs = heltec_vision_master_e290 +;default_envs = heltec_mesh_node_t114 + +extra_configs = + arch/*/*.ini + variants/*/platformio.ini + +[env] +test_build_src = true +extra_scripts = bin/platformio-custom.py + +; note: we add src to our include search path so that lmic_project_config can override +; note: TINYGPS_OPTION_NO_CUSTOM_FIELDS is VERY important. We don't use custom fields and somewhere in that pile +; of code is a heap corruption bug! +; FIXME: fix lib/BluetoothOTA dependency back on src/ so we can remove -Isrc +; The Radiolib stuff will speed up building considerably. Exclud all the stuff we dont need. +build_flags = -Wno-missing-field-initializers + -Wno-format + -Isrc -Isrc/mesh -Isrc/mesh/generated -Isrc/gps -Isrc/buzz -Wl,-Map,.pio/build/output.map + -DUSE_THREAD_NAMES + -DTINYGPS_OPTION_NO_CUSTOM_FIELDS + -DPB_ENABLE_MALLOC=1 + -DRADIOLIB_EXCLUDE_CC1101=1 + -DRADIOLIB_EXCLUDE_NRF24=1 + -DRADIOLIB_EXCLUDE_RF69=1 + -DRADIOLIB_EXCLUDE_SX1231=1 + -DRADIOLIB_EXCLUDE_SX1233=1 + -DRADIOLIB_EXCLUDE_SI443X=1 + -DRADIOLIB_EXCLUDE_RFM2X=1 + -DRADIOLIB_EXCLUDE_AFSK=1 + -DRADIOLIB_EXCLUDE_BELL=1 + -DRADIOLIB_EXCLUDE_HELLSCHREIBER=1 + -DRADIOLIB_EXCLUDE_MORSE=1 + -DRADIOLIB_EXCLUDE_RTTY=1 + -DRADIOLIB_EXCLUDE_SSTV=1 + -DRADIOLIB_EXCLUDE_AX25=1 + -DRADIOLIB_EXCLUDE_DIRECT_RECEIVE=1 + -DRADIOLIB_EXCLUDE_BELL=1 + -DRADIOLIB_EXCLUDE_PAGER=1 + -DRADIOLIB_EXCLUDE_FSK4=1 + -DRADIOLIB_EXCLUDE_APRS=1 + -DRADIOLIB_EXCLUDE_LORAWAN=1 + -DMESHTASTIC_EXCLUDE_DROPZONE=1 + -DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1 + #-DBUILD_EPOCH=$UNIX_TIME + ;-D OLED_PL + +monitor_speed = 115200 +monitor_filters = direct + +lib_deps = + jgromes/RadioLib@~7.0.2 + https://github.com/meshtastic/esp8266-oled-ssd1306.git#e16cee124fe26490cb14880c679321ad8ac89c95 ; ESP8266_SSD1306 + mathertel/OneButton@~2.6.1 ; OneButton library for non-blocking button debounce + https://github.com/meshtastic/arduino-fsm.git#7db3702bf0cfe97b783d6c72595e3f38e0b19159 + https://github.com/meshtastic/TinyGPSPlus.git#71a82db35f3b973440044c476d4bcdc673b104f4 + https://github.com/meshtastic/ArduinoThread.git#1ae8778c85d0a2a729f989e0b1e7d7c4dc84eef0 + nanopb/Nanopb@^0.4.9 + erriez/ErriezCRC32@^1.0.1 + +; Used for the code analysis in PIO Home / Inspect +check_tool = cppcheck +check_skip_packages = yes +check_flags = + -DAPP_VERSION=1.0.0 + --suppressions-list=suppressions.txt + --inline-suppr + +; Common settings for conventional (non Portduino) Arduino targets +[arduino_base] +framework = arduino +lib_deps = + ${env.lib_deps} + end2endzone/NonBlockingRTTTL@^1.3.0 + https://github.com/meshtastic/SparkFun_ATECCX08a_Arduino_Library.git#5cf62b36c6f30bc72a07bdb2c11fc9a22d1e31da + +build_flags = ${env.build_flags} -Os +build_src_filter = ${env.build_src_filter} - + +; Common libs for communicating over TCP/IP networks such as MQTT +[networking_base] +lib_deps = + knolleary/PubSubClient@^2.8 + arduino-libraries/NTPClient@^3.1.0 + arcao/Syslog@^2.0.0 + +; Common libs for environmental measurements in telemetry module +; (not included in native / portduino) +[environmental_base] +lib_deps = + adafruit/Adafruit BusIO@^1.16.1 + adafruit/Adafruit Unified Sensor@^1.1.11 + adafruit/Adafruit BMP280 Library@^2.6.8 + adafruit/Adafruit BMP085 Library@^1.2.4 + adafruit/Adafruit BME280 Library@^2.2.2 + adafruit/Adafruit BMP3XX Library@^2.1.5 + adafruit/Adafruit MCP9808 Library@^2.0.0 + adafruit/Adafruit INA260 Library@^1.5.0 + adafruit/Adafruit INA219@^1.2.0 + adafruit/Adafruit MAX1704X@^1.0.3 + adafruit/Adafruit SHTC3 Library@^1.0.0 + adafruit/Adafruit LPS2X@^2.0.4 + adafruit/Adafruit SHT31 Library@^2.2.2 + adafruit/Adafruit PM25 AQI Sensor@^1.1.1 + adafruit/Adafruit MPU6050@^2.2.4 + adafruit/Adafruit LIS3DH@^1.3.0 + adafruit/Adafruit AHTX0@^2.0.5 + adafruit/Adafruit LSM6DS@^4.7.2 + adafruit/Adafruit VEML7700 Library@^2.1.6 + adafruit/Adafruit SHT4x Library@^1.0.4 + adafruit/Adafruit TSL2591 Library@^1.4.5 + sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@^1.0.5 + sparkfun/SparkFun 9DoF IMU Breakout - ICM 20948 - Arduino Library@^1.2.13 + ClosedCube OPT3001@^1.1.2 + emotibit/EmotiBit MLX90632@^1.0.8 + dfrobot/DFRobot_RTU@^1.0.3 + sparkfun/SparkFun MAX3010x Pulse and Proximity Sensor Library@^1.1.2 + adafruit/Adafruit MLX90614 Library@^2.1.5 + + https://github.com/boschsensortec/Bosch-BSEC2-Library#v1.7.2502 + boschsensortec/BME68x Sensor Library@^1.1.40407 + https://github.com/KodinLanewave/INA3221@^1.0.1 + lewisxhe/SensorLib@0.2.0 + mprograms/QMC5883LCompass@^1.2.0 + + https://github.com/meshtastic/DFRobot_LarkWeatherStation#4de3a9cadef0f6a5220a8a906cf9775b02b0040d + https://github.com/gjelsoe/STK8xxx-Accelerometer.git#v0.1.1 diff --git a/pyocd.yaml b/pyocd.yaml new file mode 100644 index 0000000..84bd933 --- /dev/null +++ b/pyocd.yaml @@ -0,0 +1,7 @@ +# This is a config file to control pyocd ICE debugger probe options (only used for NRF52 targets with hardware debugging connections) +# for more info see FIXMEURL + +# console or telnet +semihost_console_type: telnet +enable_semihosting: True +telnet_port: 4444 diff --git a/src/AmbientLightingThread.h b/src/AmbientLightingThread.h new file mode 100644 index 0000000..07764e6 --- /dev/null +++ b/src/AmbientLightingThread.h @@ -0,0 +1,186 @@ +#include "Observer.h" +#include "configuration.h" + +#ifdef HAS_NCP5623 +#include +NCP5623 rgb; +#endif + +#ifdef HAS_NEOPIXEL +#include +Adafruit_NeoPixel pixels(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE); +#endif + +#ifdef UNPHONE +#include "unPhone.h" +extern unPhone unphone; +#endif + +namespace concurrency +{ +class AmbientLightingThread : public concurrency::OSThread +{ + public: + explicit AmbientLightingThread(ScanI2C::DeviceType type) : OSThread("AmbientLightingThread") + { + notifyDeepSleepObserver.observe(¬ifyDeepSleep); // Let us know when shutdown() is issued. + +// Enables Ambient Lighting by default if conditions are meet. +#if defined(HAS_NCP5623) || defined(RGBLED_RED) || defined(HAS_NEOPIXEL) || defined(UNPHONE) +#ifdef ENABLE_AMBIENTLIGHTING + 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; + +#ifdef HAS_NCP5623 + _type = type; + if (_type == ScanI2C::DeviceType::NONE) { + LOG_DEBUG("AmbientLightingThread disabling due to no RGB leds found on I2C bus"); + disable(); + return; + } +#endif +#if defined(HAS_NCP5623) || defined(RGBLED_RED) || defined(HAS_NEOPIXEL) || defined(UNPHONE) + if (!moduleConfig.ambient_lighting.led_state) { + LOG_DEBUG("AmbientLightingThread disabling due to moduleConfig.ambient_lighting.led_state OFF"); + disable(); + return; + } + LOG_DEBUG("AmbientLightingThread initializing"); +#ifdef HAS_NCP5623 + if (_type == ScanI2C::NCP5623) { + rgb.begin(); +#endif +#ifdef RGBLED_RED + 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); +#endif + setLighting(); +#endif +#ifdef HAS_NCP5623 + } +#endif + } + + protected: + int32_t runOnce() override + { +#if defined(HAS_NCP5623) || defined(RGBLED_RED) || defined(HAS_NEOPIXEL) || defined(UNPHONE) +#ifdef HAS_NCP5623 + if (_type == ScanI2C::NCP5623 && moduleConfig.ambient_lighting.led_state) { +#endif + setLighting(); + return 30000; // 30 seconds to reset from any animations that may have been running from Ext. Notification +#ifdef HAS_NCP5623 + } +#endif +#endif + return disable(); + } + + // When shutdown() is issued, setLightingOff will be called. + CallbackObserver notifyDeepSleepObserver = + CallbackObserver(this, &AmbientLightingThread::setLightingOff); + + private: + ScanI2C::DeviceType _type = ScanI2C::DeviceType::NONE; + + // 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("Turn Off NCP5623 Ambient lighting."); +#endif +#ifdef HAS_NEOPIXEL + pixels.clear(); + pixels.show(); + LOG_INFO("Turn 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("Turn Off Ambient lighting RGB Common Anode."); +#elif defined(RGBLED_RED) + analogWrite(RGBLED_RED, 0); + analogWrite(RGBLED_GREEN, 0); + analogWrite(RGBLED_BLUE, 0); + LOG_INFO("Turn Off Ambient lighting RGB Common Cathode."); +#endif +#ifdef UNPHONE + unphone.rgb(0, 0, 0); + LOG_INFO("Turn Off unPhone Ambient lighting."); +#endif + return 0; + } + + 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("Initializing NCP5623 Ambient lighting 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); + +// 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); +#endif +#if defined(BUTTON2_COLOR) && defined(BUTTON2_COLOR_INDEX) + pixels.fill(BUTTON2_COLOR, BUTTON1_COLOR_INDEX, 1); +#endif +#endif + pixels.show(); + LOG_DEBUG("Initializing NeoPixel Ambient lighting 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("Initializing Ambient lighting 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("Initializing Ambient lighting 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("Initializing unPhone Ambient lighting w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, + moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); +#endif + } +}; + +} // namespace concurrency \ No newline at end of file diff --git a/src/AudioThread.h b/src/AudioThread.h new file mode 100644 index 0000000..c9f2534 --- /dev/null +++ b/src/AudioThread.h @@ -0,0 +1,77 @@ +#pragma once +#include "PowerFSM.h" +#include "concurrency/OSThread.h" +#include "configuration.h" +#include "main.h" +#include "sleep.h" + +#ifdef HAS_I2S +#include +#include +#include +#include + +#define AUDIO_THREAD_INTERVAL_MS 100 + +class AudioThread : public concurrency::OSThread +{ + public: + AudioThread() : OSThread("AudioThread") { initOutput(); } + + void beginRttl(const void *data, uint32_t len) + { + setCPUFast(true); + rtttlFile = new AudioFileSourcePROGMEM(data, len); + i2sRtttl = new AudioGeneratorRTTTL(); + i2sRtttl->begin(rtttlFile, audioOut); + } + + bool isPlaying() + { + if (i2sRtttl != nullptr) { + return i2sRtttl->isRunning() && i2sRtttl->loop(); + } + return false; + } + + void stop() + { + if (i2sRtttl != nullptr) { + i2sRtttl->stop(); + delete i2sRtttl; + i2sRtttl = nullptr; + } + if (rtttlFile != nullptr) { + delete rtttlFile; + rtttlFile = nullptr; + } + + setCPUFast(false); + } + + 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; + } + + private: + void initOutput() + { + audioOut = new AudioOutputI2S(1, AudioOutputI2S::EXTERNAL_I2S); + audioOut->SetPinout(DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT); + audioOut->SetGain(0.2); + }; + + AudioGeneratorRTTTL *i2sRtttl = nullptr; + AudioOutputI2S *audioOut; + + AudioFileSourcePROGMEM *rtttlFile; +}; + +#endif diff --git a/src/BluetoothCommon.cpp b/src/BluetoothCommon.cpp new file mode 100644 index 0000000..d9502e4 --- /dev/null +++ b/src/BluetoothCommon.cpp @@ -0,0 +1,17 @@ +#include "BluetoothCommon.h" +#include "configuration.h" + +// 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 diff --git a/src/BluetoothCommon.h b/src/BluetoothCommon.h new file mode 100644 index 0000000..440d138 --- /dev/null +++ b/src/BluetoothCommon.h @@ -0,0 +1,32 @@ +#pragma once + +#include + +/** + * Common lib functions for all platforms that have bluetooth + */ + +#define MESH_SERVICE_UUID "6ba1b218-15a8-461f-9fa8-5dcae273eafd" + +#define TORADIO_UUID "f75c76d2-129e-4dad-a1dd-7866124401e7" +#define FROMRADIO_UUID "2c55e69e-4993-11ed-b878-0242ac120002" +#define FROMNUM_UUID "ed9da18c-a800-4f66-a670-aa7547e34453" +#define LEGACY_LOGRADIO_UUID "6c6fd238-78fa-436b-aacf-15c5be1ef2e2" +#define LOGRADIO_UUID "5a3d6e49-06e6-4423-9944-e9de8cdf9547" + +// NRF52 wants these constants as byte arrays +// Generated here https://yupana-engineering.com/online-uuid-to-c-array-converter - but in REVERSE BYTE ORDER +extern const uint8_t MESH_SERVICE_UUID_16[], TORADIO_UUID_16[16u], FROMRADIO_UUID_16[], FROMNUM_UUID_16[], LOGRADIO_UUID_16[]; + +/// 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; +}; \ No newline at end of file diff --git a/src/ButtonThread.cpp b/src/ButtonThread.cpp new file mode 100644 index 0000000..ca4d40e --- /dev/null +++ b/src/ButtonThread.cpp @@ -0,0 +1,335 @@ +#include "ButtonThread.h" +#include "configuration.h" +#if !MESHTASTIC_EXCLUDE_GPS +#include "GPS.h" +#endif +#include "MeshService.h" +#include "PowerFSM.h" +#include "RadioLibInterface.h" +#include "buzz.h" +#include "main.h" +#include "modules/ExternalNotificationModule.h" +#include "power.h" +#ifdef ARCH_PORTDUINO +#include "platform/portduino/PortduinoGlue.h" +#endif + +#define DEBUG_BUTTONS 0 +#if DEBUG_BUTTONS +#define LOG_BUTTON(...) LOG_DEBUG(__VA_ARGS__) +#else +#define LOG_BUTTON(...) +#endif + +using namespace concurrency; + +ButtonThread *buttonThread; // Declared extern in header +volatile ButtonThread::ButtonEventType ButtonThread::btnEvent = ButtonThread::BUTTON_EVENT_NONE; + +#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) +OneButton ButtonThread::userButton; // Get reference to static member +#endif +ButtonThread::ButtonThread() : OSThread("Button") +{ +#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) + +#if defined(ARCH_PORTDUINO) + if (settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC) { + this->userButton = OneButton(settingsMap[user], true, true); + LOG_DEBUG("Using GPIO%02d for button", settingsMap[user]); + } +#elif defined(BUTTON_PIN) + int pin = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN; // Resolved button pin +#if defined(HELTEC_CAPSULE_SENSOR_V3) + this->userButton = OneButton(pin, false, false); +#elif defined(BUTTON_ACTIVE_LOW) + this->userButton = OneButton(pin, BUTTON_ACTIVE_LOW, BUTTON_ACTIVE_PULLUP); +#else + this->userButton = OneButton(pin, true, true); +#endif + LOG_DEBUG("Using GPIO%02d for button", pin); +#endif + +#ifdef INPUT_PULLUP_SENSE + // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did +#ifdef BUTTON_SENSE_TYPE + pinMode(pin, BUTTON_SENSE_TYPE); +#else + pinMode(pin, INPUT_PULLUP_SENSE); +#endif +#endif + +#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) + userButton.attachClick(userButtonPressed); + userButton.setClickMs(BUTTON_CLICK_MS); + userButton.setPressMs(BUTTON_LONGPRESS_MS); + userButton.setDebounceMs(1); + userButton.attachDoubleClick(userButtonDoublePressed); + userButton.attachMultiClick(userButtonMultiPressed, this); // Reference to instance: get click count from non-static OneButton +#ifndef T_DECK // T-Deck immediately wakes up after shutdown, so disable this function + userButton.attachLongPressStart(userButtonPressedLongStart); + userButton.attachLongPressStop(userButtonPressedLongStop); +#endif +#endif + +#ifdef BUTTON_PIN_ALT + userButtonAlt = OneButton(BUTTON_PIN_ALT, true, true); +#ifdef INPUT_PULLUP_SENSE + // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did + pinMode(BUTTON_PIN_ALT, INPUT_PULLUP_SENSE); +#endif + userButtonAlt.attachClick(userButtonPressed); + userButtonAlt.setClickMs(BUTTON_CLICK_MS); + userButtonAlt.setPressMs(BUTTON_LONGPRESS_MS); + userButtonAlt.setDebounceMs(1); + userButtonAlt.attachDoubleClick(userButtonDoublePressed); + userButtonAlt.attachLongPressStart(userButtonPressedLongStart); + userButtonAlt.attachLongPressStop(userButtonPressedLongStop); +#endif + +#ifdef BUTTON_PIN_TOUCH + userButtonTouch = OneButton(BUTTON_PIN_TOUCH, true, true); + userButtonTouch.setPressMs(BUTTON_TOUCH_MS); + userButtonTouch.attachLongPressStart(touchPressedLongStart); // Better handling with longpress than click? +#endif + + attachButtonInterrupts(); +#endif +} + +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 + +#if defined(BUTTON_PIN) + userButton.tick(); + canSleep &= userButton.isIdle(); +#elif defined(ARCH_PORTDUINO) + if (settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC) { + userButton.tick(); + canSleep &= userButton.isIdle(); + } +#endif +#ifdef BUTTON_PIN_ALT + userButtonAlt.tick(); + canSleep &= userButtonAlt.isIdle(); +#endif +#ifdef BUTTON_PIN_TOUCH + userButtonTouch.tick(); + canSleep &= userButtonTouch.isIdle(); +#endif + + if (btnEvent != BUTTON_EVENT_NONE) { + switch (btnEvent) { + case BUTTON_EVENT_PRESSED: { + LOG_BUTTON("press!"); + // If a nag notification is running, stop it and prevent other actions + if (moduleConfig.external_notification.enabled && (externalNotificationModule->nagCycleCutoff != UINT32_MAX)) { + externalNotificationModule->stopNow(); + return 50; + } +#ifdef BUTTON_PIN + if (((config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN) != + moduleConfig.canned_message.inputbroker_pin_press) || + !(moduleConfig.canned_message.updown1_enabled || moduleConfig.canned_message.rotary1_enabled) || + !moduleConfig.canned_message.enabled) { + powerFSM.trigger(EVENT_PRESS); + } +#endif +#if defined(ARCH_PORTDUINO) + if ((settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC) && + (settingsMap[user] != moduleConfig.canned_message.inputbroker_pin_press) || + !moduleConfig.canned_message.enabled) { + powerFSM.trigger(EVENT_PRESS); + } +#endif + break; + } + + case BUTTON_EVENT_DOUBLE_PRESSED: { + LOG_BUTTON("Double press!"); + service->refreshLocalMeshNode(); + auto sentPosition = service->trySendPosition(NODENUM_BROADCAST, true); + if (screen) { + if (sentPosition) + screen->print("Sent ad-hoc position\n"); + else + screen->print("Sent ad-hoc nodeinfo\n"); + screen->forceDisplay(true); // Force a new UI frame, then force an EInk update + } + break; + } + + case BUTTON_EVENT_MULTI_PRESSED: { + LOG_BUTTON("Mulitipress! %hux", multipressClickCount); + switch (multipressClickCount) { +#if HAS_GPS + // 3 clicks: toggle GPS + case 3: + if (!config.device.disable_triple_click && (gps != nullptr)) { + gps->toggleGpsMode(); + if (screen) + screen->forceDisplay(true); // Force a new UI frame, then force an EInk update + } + break; +#endif +#if defined(USE_EINK) && defined(PIN_EINK_EN) // i.e. T-Echo + // 4 clicks: toggle backlight + case 4: + digitalWrite(PIN_EINK_EN, digitalRead(PIN_EINK_EN) == LOW); + break; +#endif + // No valid multipress action + default: + break; + } // end switch: click count + + break; + } // end multipress event + + case BUTTON_EVENT_LONG_PRESSED: { + LOG_BUTTON("Long press!"); + powerFSM.trigger(EVENT_PRESS); + if (screen) { + screen->startAlert("Shutting down..."); + } + playBeep(); + break; + } + + // Do actual shutdown when button released, otherwise the button release + // may wake the board immediatedly. + case BUTTON_EVENT_LONG_RELEASED: { + LOG_INFO("Shutdown from long press"); + playShutdownMelody(); + delay(3000); + power->shutdown(); + break; + } + +#ifdef BUTTON_PIN_TOUCH + case BUTTON_EVENT_TOUCH_LONG_PRESSED: { + LOG_BUTTON("Touch press!"); + if (screen) { + // Wake if asleep + if (powerFSM.getState() == &stateDARK) + powerFSM.trigger(EVENT_PRESS); + + // Update display (legacy behaviour) + screen->forceDisplay(); + } + break; + } +#endif // BUTTON_PIN_TOUCH + + default: + break; + } + btnEvent = BUTTON_EVENT_NONE; + } + + return 50; +} + +/* + * Attach (or re-attach) hardware interrupts for buttons + * Public method. Used outside class when waking from MCU sleep + */ +void ButtonThread::attachButtonInterrupts() +{ +#if defined(ARCH_PORTDUINO) + if (settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC) + wakeOnIrq(settingsMap[user], FALLING); +#elif defined(BUTTON_PIN) + // Interrupt for user button, during normal use. Improves responsiveness. + attachInterrupt( + config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, + []() { + ButtonThread::userButton.tick(); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }, + CHANGE); +#endif + +#ifdef BUTTON_PIN_ALT + wakeOnIrq(BUTTON_PIN_ALT, FALLING); +#endif + +#ifdef BUTTON_PIN_TOUCH + wakeOnIrq(BUTTON_PIN_TOUCH, FALLING); +#endif +} + +/* + * Detach the "normal" button interrupts. + * Public method. Used before attaching a "wake-on-button" interrupt for MCU sleep + */ +void ButtonThread::detachButtonInterrupts() +{ +#if defined(ARCH_PORTDUINO) + if (settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC) + detachInterrupt(settingsMap[user]); +#elif defined(BUTTON_PIN) + detachInterrupt(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN); +#endif + +#ifdef BUTTON_PIN_ALT + detachInterrupt(BUTTON_PIN_ALT); +#endif + +#ifdef BUTTON_PIN_TOUCH + detachInterrupt(BUTTON_PIN_TOUCH); +#endif +} + +/** + * Watch a GPIO and if we get an IRQ, wake the main thread. + * Use to add wake on button press + */ +void ButtonThread::wakeOnIrq(int irq, int mode) +{ + attachInterrupt( + irq, + [] { + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + runASAP = true; + }, + FALLING); +} + +// Static callback +void ButtonThread::userButtonMultiPressed(void *callerThread) +{ + // Grab click count from non-static button, while the info is still valid + ButtonThread *thread = (ButtonThread *)callerThread; + thread->storeClickCount(); + + // Then handle later, in the usual way + btnEvent = BUTTON_EVENT_MULTI_PRESSED; +} + +// Non-static method, runs during callback. Grabs info while still valid +void ButtonThread::storeClickCount() +{ +#ifdef BUTTON_PIN + multipressClickCount = userButton.getNumberClicks(); +#endif +} + +void ButtonThread::userButtonPressedLongStart() +{ + if (millis() > c_holdOffTime) { + btnEvent = BUTTON_EVENT_LONG_PRESSED; + } +} + +void ButtonThread::userButtonPressedLongStop() +{ + if (millis() > c_holdOffTime) { + btnEvent = BUTTON_EVENT_LONG_RELEASED; + } +} \ No newline at end of file diff --git a/src/ButtonThread.h b/src/ButtonThread.h new file mode 100644 index 0000000..9cd7b3d --- /dev/null +++ b/src/ButtonThread.h @@ -0,0 +1,68 @@ +#pragma once + +#include "OneButton.h" +#include "concurrency/OSThread.h" +#include "configuration.h" + +#ifndef BUTTON_CLICK_MS +#define BUTTON_CLICK_MS 250 +#endif + +#ifndef BUTTON_LONGPRESS_MS +#define BUTTON_LONGPRESS_MS 5000 +#endif + +#ifndef BUTTON_TOUCH_MS +#define BUTTON_TOUCH_MS 400 +#endif + +class ButtonThread : public concurrency::OSThread +{ + public: + static const uint32_t c_holdOffTime = 30000; // hold off 30s after boot + + enum ButtonEventType { + BUTTON_EVENT_NONE, + BUTTON_EVENT_PRESSED, + BUTTON_EVENT_DOUBLE_PRESSED, + BUTTON_EVENT_MULTI_PRESSED, + BUTTON_EVENT_LONG_PRESSED, + BUTTON_EVENT_LONG_RELEASED, + BUTTON_EVENT_TOUCH_LONG_PRESSED, + }; + + ButtonThread(); + int32_t runOnce() override; + void attachButtonInterrupts(); + void detachButtonInterrupts(); + void storeClickCount(); + + private: +#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) + static OneButton userButton; // Static - accessed from an interrupt +#endif +#ifdef BUTTON_PIN_ALT + OneButton userButtonAlt; +#endif +#ifdef BUTTON_PIN_TOUCH + OneButton userButtonTouch; +#endif + + // set during IRQ + static volatile ButtonEventType btnEvent; + + // Store click count during callback, for later use + volatile int multipressClickCount = 0; + + static void wakeOnIrq(int irq, int mode); + + // IRQ callbacks + static void userButtonPressed() { btnEvent = BUTTON_EVENT_PRESSED; } + static void userButtonDoublePressed() { btnEvent = BUTTON_EVENT_DOUBLE_PRESSED; } + static void userButtonMultiPressed(void *callerThread); // Retrieve click count from non-static Onebutton while still valid + static void userButtonPressedLongStart(); + static void userButtonPressedLongStop(); + static void touchPressedLongStart() { btnEvent = BUTTON_EVENT_TOUCH_LONG_PRESSED; } +}; + +extern ButtonThread *buttonThread; diff --git a/src/DebugConfiguration.cpp b/src/DebugConfiguration.cpp new file mode 100644 index 0000000..1c081ae --- /dev/null +++ b/src/DebugConfiguration.cpp @@ -0,0 +1,198 @@ +/* based on https://github.com/arcao/Syslog + +MIT License + +Copyright (c) 2016 Martin Sloup + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.*/ + +#include "configuration.h" + +#include "DebugConfiguration.h" + +#ifdef ARCH_PORTDUINO +#include "platform/portduino/PortduinoGlue.h" +#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); +} + +#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)) { + 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::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::defaultPriority(uint16_t pri) +{ + this->_priDefault = pri; + 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::disable() +{ + this->_enabled = false; + this->_client->stop(); +} + +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 *appName, const char *fmt, va_list args) +{ + char *message; + size_t initialLen; + size_t len; + bool result; + + 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); + + delete[] message; + return result; +} + +inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *message) +{ + int result; +#ifdef ARCH_PORTDUINO + bool utf = !settingsMap[ascii_logs]; +#else + bool utf = true; +#endif + + 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(); + + return true; +} + +#endif diff --git a/src/DebugConfiguration.h b/src/DebugConfiguration.h new file mode 100644 index 0000000..55453ea --- /dev/null +++ b/src/DebugConfiguration.h @@ -0,0 +1,167 @@ +#pragma once + +#include "configuration.h" + +// DEBUG LED +#ifndef LED_STATE_ON +#define LED_STATE_ON 1 +#endif + +// ----------------------------------------------------------------------------- +// DEBUG +// ----------------------------------------------------------------------------- + +#ifdef CONSOLE_MAX_BAUD +#define SERIAL_BAUD CONSOLE_MAX_BAUD +#else +#define SERIAL_BAUD 115200 // Serial debug baud rate +#endif + +#define MESHTASTIC_LOG_LEVEL_DEBUG "DEBUG" +#define MESHTASTIC_LOG_LEVEL_INFO "INFO " +#define MESHTASTIC_LOG_LEVEL_WARN "WARN " +#define MESHTASTIC_LOG_LEVEL_ERROR "ERROR" +#define MESHTASTIC_LOG_LEVEL_CRIT "CRIT " +#define MESHTASTIC_LOG_LEVEL_TRACE "TRACE" + +#include "SerialConsole.h" + +// If defined we will include support for ARM ICE "semihosting" for a virtual +// console over the JTAG port (to replace the normal serial port) +// Note: Normally this flag is passed into the gcc commandline by platformio.ini. +// for an example see env:rak4631_dap. +// #ifndef USE_SEMIHOSTING +// #define USE_SEMIHOSTING +// #endif + +#define DEBUG_PORT (*console) // Serial debug port + +#ifdef USE_SEGGER +// #undef DEBUG_PORT +#define LOG_DEBUG(...) SEGGER_RTT_printf(0, __VA_ARGS__) +#define LOG_INFO(...) SEGGER_RTT_printf(0, __VA_ARGS__) +#define LOG_WARN(...) SEGGER_RTT_printf(0, __VA_ARGS__) +#define LOG_ERROR(...) SEGGER_RTT_printf(0, __VA_ARGS__) +#define LOG_CRIT(...) SEGGER_RTT_printf(0, __VA_ARGS__) +#define LOG_TRACE(...) SEGGER_RTT_printf(0, __VA_ARGS__) +#else +#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) && !defined(PIO_UNIT_TESTING) +#define LOG_DEBUG(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_DEBUG, __VA_ARGS__) +#define LOG_INFO(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_INFO, __VA_ARGS__) +#define LOG_WARN(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_WARN, __VA_ARGS__) +#define LOG_ERROR(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_ERROR, __VA_ARGS__) +#define LOG_CRIT(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_CRIT, __VA_ARGS__) +#define LOG_TRACE(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_TRACE, __VA_ARGS__) +#else +#define LOG_DEBUG(...) +#define LOG_INFO(...) +#define LOG_WARN(...) +#define LOG_ERROR(...) +#define LOG_CRIT(...) +#define LOG_TRACE(...) +#endif +#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, ...); + +#define SYSLOG_NILVALUE "-" + +#define SYSLOG_CRIT 2 /* critical conditions */ +#define SYSLOG_ERR 3 /* error conditions */ +#define SYSLOG_WARN 4 /* warning conditions */ +#define SYSLOG_INFO 6 /* informational */ +#define SYSLOG_DEBUG 7 /* debug-level messages */ +// trace does not go out to syslog (yet?) + +#define LOG_PRIMASK 0x07 /* mask to extract priority part (internal) */ + /* extract priority */ +#define LOG_PRI(p) ((p)&LOG_PRIMASK) +#define LOG_MAKEPRI(fac, pri) (((fac) << 3) | (pri)) + +/* facility codes */ +#define LOGLEVEL_KERN (0 << 3) /* kernel messages */ +#define LOGLEVEL_USER (1 << 3) /* random user-level messages */ +#define LOGLEVEL_MAIL (2 << 3) /* mail system */ +#define LOGLEVEL_DAEMON (3 << 3) /* system daemons */ +#define LOGLEVEL_AUTH (4 << 3) /* security/authorization messages */ +#define LOGLEVEL_SYSLOG (5 << 3) /* messages generated internally by syslogd */ +#define LOGLEVEL_LPR (6 << 3) /* line printer subsystem */ +#define LOGLEVEL_NEWS (7 << 3) /* network news subsystem */ +#define LOGLEVEL_UUCP (8 << 3) /* UUCP subsystem */ +#define LOGLEVEL_CRON (9 << 3) /* clock daemon */ +#define LOGLEVEL_AUTHPRIV (10 << 3) /* security/authorization messages (private) */ +#define LOGLEVEL_FTP (11 << 3) /* ftp daemon */ + +/* other codes through 15 reserved for system use */ +#define LOGLEVEL_LOCAL0 (16 << 3) /* reserved for local use */ +#define LOGLEVEL_LOCAL1 (17 << 3) /* reserved for local use */ +#define LOGLEVEL_LOCAL2 (18 << 3) /* reserved for local use */ +#define LOGLEVEL_LOCAL3 (19 << 3) /* reserved for local use */ +#define LOGLEVEL_LOCAL4 (20 << 3) /* reserved for local use */ +#define LOGLEVEL_LOCAL5 (21 << 3) /* reserved for local use */ +#define LOGLEVEL_LOCAL6 (22 << 3) /* reserved for local use */ +#define LOGLEVEL_LOCAL7 (23 << 3) /* reserved for local use */ + +#define LOG_NFACILITIES 24 /* current number of facilities */ +#define LOG_FACMASK 0x03f8 /* mask to extract facility part */ + /* facility of pri */ +#define LOG_FAC(p) (((p)&LOG_FACMASK) >> 3) + +#define LOG_MASK(pri) (1 << (pri)) /* mask for one priority */ +#define LOG_UPTO(pri) ((1 << ((pri) + 1)) - 1) /* all priorities through pri */ + +// ----------------------------------------------------------------------------- +// AXP192 (Rev1-specific options) +// ----------------------------------------------------------------------------- + +#define GPS_POWER_CTRL_CH 3 +#define LORA_POWER_CTRL_CH 2 + +// Default Bluetooth PIN +#define defaultBLEPin 123456 + +#if HAS_ETHERNET +#include +#endif // HAS_ETHERNET + +#if HAS_WIFI +#include +#endif // HAS_WIFI + +#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; + + bool _sendLog(uint16_t pri, const char *appName, const char *message); + + 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); + + 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))); +}; + +#endif // HAS_ETHERNET || HAS_WIFI \ No newline at end of file diff --git a/src/DisplayFormatters.cpp b/src/DisplayFormatters.cpp new file mode 100644 index 0000000..0718ffc --- /dev/null +++ b/src/DisplayFormatters.cpp @@ -0,0 +1,37 @@ +#include "DisplayFormatters.h" + +const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName) +{ + 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_MODERATE: + return useShortName ? "LongM" : "LongMod"; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_VERY_LONG_SLOW: + return useShortName ? "VeryL" : "VLongSlow"; + break; + default: + return useShortName ? "Custom" : "Invalid"; + break; + } +} \ No newline at end of file diff --git a/src/DisplayFormatters.h b/src/DisplayFormatters.h new file mode 100644 index 0000000..f8ccfcb --- /dev/null +++ b/src/DisplayFormatters.h @@ -0,0 +1,8 @@ +#pragma once +#include "NodeDB.h" + +class DisplayFormatters +{ + public: + static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName); +}; diff --git a/src/FSCommon.cpp b/src/FSCommon.cpp new file mode 100644 index 0000000..cea1130 --- /dev/null +++ b/src/FSCommon.cpp @@ -0,0 +1,377 @@ +/** + * @file FSCommon.cpp + * @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. + * + */ +#include "FSCommon.h" +#include "configuration.h" + +#ifdef HAS_SDCARD +#include +#include + +#ifdef SDCARD_USE_SPI1 +SPIClass SPI1(HSPI); +#define SDHandler SPI1 +#else +#define SDHandler SPI +#endif + +#endif // HAS_SDCARD + +#if defined(ARCH_STM32WL) + +uint16_t OSFS::startOfEEPROM = 1; +uint16_t OSFS::endOfEEPROM = 2048; + +// 3) How do I read from the medium? +void OSFS::readNBytes(uint16_t address, unsigned int num, byte *output) +{ + for (uint16_t i = address; i < address + num; i++) { + *output = EEPROM.read(i); + output++; + } +} + +// 4) How to I write to the medium? +void OSFS::writeNBytes(uint16_t address, unsigned int num, const byte *input) +{ + for (uint16_t i = address; i < address + num; i++) { + EEPROM.update(i, *input); + input++; + } +} +#endif + +bool lfs_assert_failed = + false; // Note: we use this global on all platforms, though it can only be set true on nrf52 (in our modified lfs_util.h) + +extern "C" void lfs_assert(const char *reason) +{ + LOG_ERROR("LFS assert: %s", reason); + lfs_assert_failed = true; +} + +/** + * @brief Copies a file from one location to another. + * + * @param from The path of the source file. + * @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) +{ +#ifdef ARCH_STM32WL + unsigned char cbuffer[2048]; + + // Var to hold the result of actions + OSFS::result r; + + r = OSFS::getFile(from, cbuffer); + + if (r == notfound) { + LOG_ERROR("Failed to open source file %s", from); + return false; + } else if (r == noerr) { + r = OSFS::newFile(to, cbuffer, true); + if (r == noerr) { + return true; + } else { + LOG_ERROR("OSFS Error %d", r); + return false; + } + + } else { + LOG_ERROR("OSFS Error %d", r); + return false; + } + return true; + +#elif defined(FSCom) + 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 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); + } + + f2.flush(); + f2.close(); + f1.close(); + return true; +#endif +} + +/** + * Renames a file from pathFrom to pathTo. + * + * @param pathFrom The original path of the file. + * @param pathTo The new path of the file. + * + * @return True if the file was successfully renamed, false otherwise. + */ +bool renameFile(const char *pathFrom, const char *pathTo) +{ +#ifdef ARCH_STM32WL + if (copyFile(pathFrom, pathTo) && (OSFS::deleteFile(pathFrom) == OSFS::result::NO_ERROR)) { + return true; + } else { + return false; + } +#elif defined(FSCom) +#ifdef ARCH_ESP32 + // rename was fixed for ESP32 IDF LittleFS in April + return FSCom.rename(pathFrom, pathTo); +#else + if (copyFile(pathFrom, pathTo) && FSCom.remove(pathFrom)) { + return true; + } else { + return false; + } +#endif +#endif +} + +#include + +/** + * @brief Get the list of files in a directory. + * + * This function returns a list of files in a directory. The list includes the full path of each file. + * + * @param dirname The name of the directory. + * @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 = {}; +#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 = {"", 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; +} + +/** + * Lists the contents of a directory. + * + * @param dirname The name of the directory to list. + * @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) +{ +#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("Removing %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("Removing %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("Deleting %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("Deleting %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("Removing %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("Removing %s", root.name()); + strncpy(buffer, root.name(), sizeof(buffer)); + root.close(); + FSCom.rmdir(buffer); + } else { + root.close(); + } +#else + root.close(); +#endif +#endif +} + +/** + * @brief Removes a directory and all its contents. + * + * This function recursively removes a directory and all its contents, including subdirectories and files. + * + * @param dirname The name of the directory to remove. + */ +void rmDir(const char *dirname) +{ +#ifdef FSCom +#if (defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) + listDir(dirname, 10, true); +#elif defined(ARCH_NRF52) + // nRF52 implementation of LittleFS has a recursive delete function + FSCom.rmdir_r(dirname); +#endif +#endif +} + +void fsInit() +{ +#ifdef FSCom + 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()); +#else + LOG_DEBUG("Filesystem files:"); +#endif + listDir("/", 10); +#endif +} + +/** + * Initializes the SD card and mounts the file system. + */ +void setupSDCard() +{ +#ifdef HAS_SDCARD + SDHandler.begin(SPI_SCK, SPI_MISO, SPI_MOSI); + + if (!SD.begin(SDCARD_CS, SDHandler)) { + 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))); +#endif +} \ No newline at end of file diff --git a/src/FSCommon.h b/src/FSCommon.h new file mode 100644 index 0000000..254245b --- /dev/null +++ b/src/FSCommon.h @@ -0,0 +1,63 @@ +#pragma once + +#include "configuration.h" +#include + +// Cross platform filesystem API + +#if defined(ARCH_PORTDUINO) +// Portduino version +#include "PortduinoFS.h" +#define FSCom PortduinoFS +#define FSBegin() true +#define FILE_O_WRITE "w" +#define FILE_O_READ "r" +#endif + +#if defined(ARCH_STM32WL) +// STM32WL series 2 Kbytes (8 rows of 256 bytes) +#include +#include + +// Useful consts +const OSFS::result noerr = OSFS::result::NO_ERROR; +const OSFS::result notfound = OSFS::result::FILE_NOT_FOUND; +#endif + +#if defined(ARCH_RP2040) +// RP2040 +#include "LittleFS.h" +#define FSCom LittleFS +#define FSBegin() FSCom.begin() // set autoformat +#define FILE_O_WRITE "w" +#define FILE_O_READ "r" +#endif + +#if defined(ARCH_ESP32) +// ESP32 version +#include "LittleFS.h" +#define FSCom LittleFS +#define FSBegin() FSCom.begin(true) // format on failure +#define FILE_O_WRITE "w" +#define FILE_O_READ "r" +#endif + +#if defined(ARCH_NRF52) +// NRF52 version +#include "InternalFileSystem.h" +#define FSCom InternalFS +#define FSBegin() FSCom.begin() // InternalFS formats on failure +using namespace Adafruit_LittleFS_Namespace; +#endif + +void fsInit(); +void fsListFiles(); +bool copyFile(const char *from, const char *to); +bool renameFile(const char *pathFrom, const char *pathTo); +std::vector getFiles(const char *dirname, uint8_t levels); +void listDir(const char *dirname, uint8_t levels, bool del = false); +void rmDir(const char *dirname); +void setupSDCard(); + +extern bool lfs_assert_failed; // Note: we use this global on all platforms, though it can only be set true on nrf52 (in our + // modified lfs_util.h) diff --git a/src/Fusion/Fusion.h b/src/Fusion/Fusion.h new file mode 100644 index 0000000..48f5198 --- /dev/null +++ b/src/Fusion/Fusion.h @@ -0,0 +1,32 @@ +/** + * @file Fusion.h + * @author Seb Madgwick + * @brief Main header file for the Fusion library. This is the only file that + * needs to be included when using the library. + */ + +#ifndef FUSION_H +#define FUSION_H + +//------------------------------------------------------------------------------ +// Includes + +#ifdef __cplusplus +extern "C" { +#endif + +#include "FusionAhrs.h" +#include "FusionAxes.h" +#include "FusionCalibration.h" +#include "FusionCompass.h" +#include "FusionConvention.h" +#include "FusionMath.h" +#include "FusionOffset.h" + +#ifdef __cplusplus +} +#endif + +#endif +//------------------------------------------------------------------------------ +// End of file diff --git a/src/Fusion/FusionAhrs.c b/src/Fusion/FusionAhrs.c new file mode 100644 index 0000000..d6c1d02 --- /dev/null +++ b/src/Fusion/FusionAhrs.c @@ -0,0 +1,542 @@ +/** + * @file FusionAhrs.c + * @author Seb Madgwick + * @brief AHRS algorithm to combine gyroscope, accelerometer, and magnetometer + * measurements into a single measurement of orientation relative to the Earth. + */ + +//------------------------------------------------------------------------------ +// Includes + +#include "FusionAhrs.h" +#include // FLT_MAX +#include // atan2f, cosf, fabsf, powf, sinf + +//------------------------------------------------------------------------------ +// Definitions + +/** + * @brief Initial gain used during the initialisation. + */ +#define INITIAL_GAIN (10.0f) + +/** + * @brief Initialisation period in seconds. + */ +#define INITIALISATION_PERIOD (3.0f) + +//------------------------------------------------------------------------------ +// Function declarations + +static inline FusionVector HalfGravity(const FusionAhrs *const ahrs); + +static inline FusionVector HalfMagnetic(const FusionAhrs *const ahrs); + +static inline FusionVector Feedback(const FusionVector sensor, const FusionVector reference); + +static inline int Clamp(const int value, const int min, const int max); + +//------------------------------------------------------------------------------ +// Functions + +/** + * @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); +} + +/** + * @brief Resets the AHRS algorithm. This is equivalent to reinitialising the + * 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; +} + +/** + * @brief Sets the AHRS algorithm settings. + * @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; +} + +/** + * @brief Updates the AHRS algorithm using the gyroscope, accelerometer, and + * magnetometer measurements. + * @param ahrs AHRS algorithm structure. + * @param gyroscope Gyroscope measurement in degrees per second. + * @param accelerometer Accelerometer measurement in g. + * @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) +{ +#define Q ahrs->quaternion.element + + // 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; + } + + // 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; + } + + // 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)); + + // 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))); + + // Normalise quaternion + ahrs->quaternion = FusionQuaternionNormalise(ahrs->quaternion); +#undef Q +} + +/** + * @brief Returns the direction of gravity scaled by 0.5. + * @param ahrs AHRS algorithm structure. + * @return Direction of gravity scaled by 0.5. + */ +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 +#undef Q +} + +/** + * @brief Returns the direction of the magnetic field scaled by 0.5. + * @param ahrs AHRS algorithm structure. + * @return Direction of the magnetic field scaled by 0.5. + */ +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 +#undef Q +} + +/** + * @brief Returns the feedback. + * @param sensor Sensor. + * @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); +} + +/** + * @brief Returns a value limited to maximum and minimum. + * @param value Value. + * @param min Minimum value. + * @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; +} + +/** + * @brief Updates the AHRS algorithm using the gyroscope and accelerometer + * measurements only. + * @param ahrs AHRS algorithm structure. + * @param gyroscope Gyroscope measurement in degrees per second. + * @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) +{ + + // Update AHRS algorithm + FusionAhrsUpdate(ahrs, gyroscope, accelerometer, FUSION_VECTOR_ZERO, deltaTime); + + // Zero heading during initialisation + if (ahrs->initialising) { + FusionAhrsSetHeading(ahrs, 0.0f); + } +} + +/** + * @brief Updates the AHRS algorithm using the gyroscope, accelerometer, and + * heading measurements. + * @param ahrs AHRS algorithm structure. + * @param gyroscope Gyroscope measurement in degrees per second. + * @param accelerometer Accelerometer measurement in 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) +{ +#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 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); +#undef Q +} + +/** + * @brief Returns the quaternion describing the sensor relative to the Earth. + * @param ahrs AHRS algorithm structure. + * @return Quaternion describing the sensor relative to the Earth. + */ +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; +} + +/** + * @brief Returns the linear acceleration measurement equal to the accelerometer + * measurement with the 1 g of gravity removed. + * @param ahrs AHRS algorithm structure. + * @return Linear acceleration measurement in g. + */ +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 + + // 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 +} + +/** + * @brief Returns the Earth acceleration measurement equal to accelerometer + * measurement in the Earth coordinate frame with the 1 g of gravity removed. + * @param ahrs AHRS algorithm structure. + * @return Earth acceleration measurement in g. + */ +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 + + // 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 +} + +/** + * @brief Returns the AHRS algorithm internal states. + * @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; +} + +/** + * @brief Returns the AHRS algorithm flags. + * @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; +} + +/** + * @brief Sets the heading of the orientation measurement provided by the AHRS + * algorithm. This function can be used to reset drift in heading when the AHRS + * algorithm is being used without a magnetometer. + * @param ahrs AHRS algorithm structure. + * @param heading Heading angle in degrees. + */ +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); +#undef Q +} + +//------------------------------------------------------------------------------ +// End of file diff --git a/src/Fusion/FusionAhrs.h b/src/Fusion/FusionAhrs.h new file mode 100644 index 0000000..aa2326e --- /dev/null +++ b/src/Fusion/FusionAhrs.h @@ -0,0 +1,112 @@ +/** + * @file FusionAhrs.h + * @author Seb Madgwick + * @brief AHRS algorithm to combine gyroscope, accelerometer, and magnetometer + * measurements into a single measurement of orientation relative to the Earth. + */ + +#ifndef FUSION_AHRS_H +#define FUSION_AHRS_H + +//------------------------------------------------------------------------------ +// Includes + +#include "FusionConvention.h" +#include "FusionMath.h" +#include + +//------------------------------------------------------------------------------ +// Definitions + +/** + * @brief AHRS algorithm settings. + */ +typedef struct { + FusionConvention convention; + float gain; + float gyroscopeRange; + float accelerationRejection; + float magneticRejection; + unsigned int recoveryTriggerPeriod; +} FusionAhrsSettings; + +/** + * @brief AHRS algorithm structure. Structure members are used internally and + * 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; +} FusionAhrs; + +/** + * @brief AHRS algorithm internal states. + */ +typedef struct { + 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; +} FusionAhrsFlags; + +//------------------------------------------------------------------------------ +// Function declarations + +void FusionAhrsInitialise(FusionAhrs *const ahrs); + +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 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); + +FusionQuaternion FusionAhrsGetQuaternion(const FusionAhrs *const ahrs); + +void FusionAhrsSetQuaternion(FusionAhrs *const ahrs, const FusionQuaternion quaternion); + +FusionVector FusionAhrsGetLinearAcceleration(const FusionAhrs *const ahrs); + +FusionVector FusionAhrsGetEarthAcceleration(const FusionAhrs *const ahrs); + +FusionAhrsInternalStates FusionAhrsGetInternalStates(const FusionAhrs *const ahrs); + +FusionAhrsFlags FusionAhrsGetFlags(const FusionAhrs *const ahrs); + +void FusionAhrsSetHeading(FusionAhrs *const ahrs, const float heading); + +#endif + +//------------------------------------------------------------------------------ +// End of file diff --git a/src/Fusion/FusionAxes.h b/src/Fusion/FusionAxes.h new file mode 100644 index 0000000..9673c88 --- /dev/null +++ b/src/Fusion/FusionAxes.h @@ -0,0 +1,188 @@ +/** + * @file FusionAxes.h + * @author Seb Madgwick + * @brief Swaps sensor axes for alignment with the body axes. + */ + +#ifndef FUSION_AXES_H +#define FUSION_AXES_H + +//------------------------------------------------------------------------------ +// Includes + +#include "FusionMath.h" + +//------------------------------------------------------------------------------ +// Definitions + +/** + * @brief Axes alignment describing the sensor axes relative to the body axes. + * For example, if the body X axis is aligned with the sensor Y axis and the + * body Y axis is aligned with sensor X axis but pointing the opposite direction + * 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 */ +} FusionAxesAlignment; + +//------------------------------------------------------------------------------ +// Inline functions + +/** + * @brief Swaps sensor axes for alignment with the body axes. + * @param sensor Sensor axes. + * @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 +} + +#endif + +//------------------------------------------------------------------------------ +// End of file diff --git a/src/Fusion/FusionCalibration.h b/src/Fusion/FusionCalibration.h new file mode 100644 index 0000000..be7102b --- /dev/null +++ b/src/Fusion/FusionCalibration.h @@ -0,0 +1,49 @@ +/** + * @file FusionCalibration.h + * @author Seb Madgwick + * @brief Gyroscope, accelerometer, and magnetometer calibration models. + */ + +#ifndef FUSION_CALIBRATION_H +#define FUSION_CALIBRATION_H + +//------------------------------------------------------------------------------ +// Includes + +#include "FusionMath.h" + +//------------------------------------------------------------------------------ +// Inline functions + +/** + * @brief Gyroscope and accelerometer calibration model. + * @param uncalibrated Uncalibrated measurement. + * @param misalignment Misalignment matrix. + * @param sensitivity Sensitivity. + * @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)); +} + +/** + * @brief Magnetometer calibration model. + * @param uncalibrated Uncalibrated measurement. + * @param softIronMatrix Soft-iron matrix. + * @param hardIronOffset Hard-iron offset. + * @return Calibrated measurement. + */ +static inline FusionVector FusionCalibrationMagnetic(const FusionVector uncalibrated, const FusionMatrix softIronMatrix, + const FusionVector hardIronOffset) +{ + return FusionMatrixMultiplyVector(softIronMatrix, FusionVectorSubtract(uncalibrated, hardIronOffset)); +} + +#endif + +//------------------------------------------------------------------------------ +// End of file diff --git a/src/Fusion/FusionCompass.c b/src/Fusion/FusionCompass.c new file mode 100644 index 0000000..6a6f959 --- /dev/null +++ b/src/Fusion/FusionCompass.c @@ -0,0 +1,51 @@ +/** + * @file FusionCompass.c + * @author Seb Madgwick + * @brief Tilt-compensated compass to calculate the magnetic heading using + * accelerometer and magnetometer measurements. + */ + +//------------------------------------------------------------------------------ +// Includes + +#include "FusionCompass.h" +#include "FusionAxes.h" +#include // atan2f + +//------------------------------------------------------------------------------ +// Functions + +/** + * @brief Calculates the magnetic heading. + * @param convention Earth axes convention. + * @param accelerometer Accelerometer measurement in any calibrated units. + * @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 +} + +//------------------------------------------------------------------------------ +// End of file diff --git a/src/Fusion/FusionCompass.h b/src/Fusion/FusionCompass.h new file mode 100644 index 0000000..a3d0b46 --- /dev/null +++ b/src/Fusion/FusionCompass.h @@ -0,0 +1,26 @@ +/** + * @file FusionCompass.h + * @author Seb Madgwick + * @brief Tilt-compensated compass to calculate the magnetic heading using + * accelerometer and magnetometer measurements. + */ + +#ifndef FUSION_COMPASS_H +#define FUSION_COMPASS_H + +//------------------------------------------------------------------------------ +// Includes + +#include "FusionConvention.h" +#include "FusionMath.h" + +//------------------------------------------------------------------------------ +// Function declarations + +float FusionCompassCalculateHeading(const FusionConvention convention, const FusionVector accelerometer, + const FusionVector magnetometer); + +#endif + +//------------------------------------------------------------------------------ +// End of file diff --git a/src/Fusion/FusionConvention.h b/src/Fusion/FusionConvention.h new file mode 100644 index 0000000..0b0d43a --- /dev/null +++ b/src/Fusion/FusionConvention.h @@ -0,0 +1,25 @@ +/** + * @file FusionConvention.h + * @author Seb Madgwick + * @brief Earth axes convention. + */ + +#ifndef FUSION_CONVENTION_H +#define FUSION_CONVENTION_H + +//------------------------------------------------------------------------------ +// Definitions + +/** + * @brief Earth axes convention. + */ +typedef enum { + FusionConventionNwu, /* North-West-Up */ + FusionConventionEnu, /* East-North-Up */ + FusionConventionNed, /* North-East-Down */ +} FusionConvention; + +#endif + +//------------------------------------------------------------------------------ +// End of file diff --git a/src/Fusion/FusionMath.h b/src/Fusion/FusionMath.h new file mode 100644 index 0000000..c3fc34b --- /dev/null +++ b/src/Fusion/FusionMath.h @@ -0,0 +1,503 @@ +/** + * @file FusionMath.h + * @author Seb Madgwick + * @brief Math library. + */ + +#ifndef FUSION_MATH_H +#define FUSION_MATH_H + +//------------------------------------------------------------------------------ +// Includes + +#include // M_PI, sqrtf, atan2f, asinf +#include +#include + +//------------------------------------------------------------------------------ +// Definitions + +/** + * @brief 3D vector. + */ +typedef union { + float array[3]; + + struct { + float x; + float y; + float z; + } axis; +} FusionVector; + +/** + * @brief Quaternion. + */ +typedef union { + float array[4]; + + struct { + float w; + float x; + float y; + float z; + } element; +} FusionQuaternion; + +/** + * @brief 3x3 matrix in row-major order. + * See http://en.wikipedia.org/wiki/Row-major_order + */ +typedef union { + float array[3][3]; + + struct { + float xx; + float xy; + float xz; + float yx; + float yy; + float yz; + float zx; + float zy; + float zz; + } element; +} FusionMatrix; + +/** + * @brief Euler angles. Roll, pitch, and yaw correspond to rotations around + * X, Y, and Z respectively. + */ +typedef union { + float array[3]; + + struct { + float roll; + float pitch; + float yaw; + } angle; +} FusionEuler; + +/** + * @brief Vector of zeros. + */ +#define FUSION_VECTOR_ZERO ((FusionVector){.array = {0.0f, 0.0f, 0.0f}}) + +/** + * @brief Vector of ones. + */ +#define FUSION_VECTOR_ONES ((FusionVector){.array = {1.0f, 1.0f, 1.0f}}) + +/** + * @brief Identity quaternion. + */ +#define FUSION_IDENTITY_QUATERNION ((FusionQuaternion){.array = {1.0f, 0.0f, 0.0f, 0.0f}}) + +/** + * @brief Identity matrix. + */ +#define FUSION_IDENTITY_MATRIX ((FusionMatrix){.array = {{1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}}}) + +/** + * @brief Euler angles of zero. + */ +#define FUSION_EULER_ZERO ((FusionEuler){.array = {0.0f, 0.0f, 0.0f}}) + +/** + * @brief Pi. May not be defined in math.h. + */ +#ifndef M_PI +#define M_PI (3.14159265358979323846) +#endif + +/** + * @brief Include this definition or add as a preprocessor definition to use + * normal square root operations. + */ +// #define FUSION_USE_NORMAL_SQRT + +//------------------------------------------------------------------------------ +// Inline functions - Degrees and radians conversion + +/** + * @brief Converts degrees to radians. + * @param degrees Degrees. + * @return Radians. + */ +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); +} + +//------------------------------------------------------------------------------ +// Inline functions - Arc sine + +/** + * @brief Returns the arc sine of the value. + * @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); +} + +//------------------------------------------------------------------------------ +// Inline functions - Fast inverse square root + +#ifndef FUSION_USE_NORMAL_SQRT + +/** + * @brief Calculates the reciprocal of the square root. + * See https://pizer.wordpress.com/2008/10/12/fast-inverse-square-root/ + * @param x Operand. + * @return Reciprocal of the square root of x. + */ +static inline float FusionFastInverseSqrt(const float x) +{ + + 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); +} + +#endif + +//------------------------------------------------------------------------------ +// Inline functions - Vector operations + +/** + * @brief Returns true if the vector is zero. + * @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); +} + +/** + * @brief Returns the sum of two vectors. + * @param vectorA Vector A. + * @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; +} + +/** + * @brief Returns vector B subtracted from vector A. + * @param vectorA Vector A. + * @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; +} + +/** + * @brief Returns the sum of the elements. + * @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; +} + +/** + * @brief Returns the multiplication of a vector by a scalar. + * @param vector 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; +} + +/** + * @brief Calculates the Hadamard product (element-wise multiplication). + * @param vectorA Vector A. + * @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; +} + +/** + * @brief Returns the cross product. + * @param vectorA Vector A. + * @param vectorB Vector B. + * @return Cross product. + */ +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; +#undef A +#undef B +} + +/** + * @brief Returns the dot product. + * @param vectorA Vector A. + * @param vectorB Vector B. + * @return Dot product. + */ +static inline float FusionVectorDotProduct(const FusionVector vectorA, const FusionVector vectorB) +{ + return FusionVectorSum(FusionVectorHadamardProduct(vectorA, vectorB)); +} + +/** + * @brief Returns the vector magnitude squared. + * @param vector Vector. + * @return Vector magnitude squared. + */ +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)); +} + +/** + * @brief Returns the normalised vector. + * @param vector Vector. + * @return Normalised vector. + */ +static inline FusionVector FusionVectorNormalise(const FusionVector vector) +{ +#ifdef FUSION_USE_NORMAL_SQRT + const float magnitudeReciprocal = 1.0f / sqrtf(FusionVectorMagnitudeSquared(vector)); +#else + const float magnitudeReciprocal = FusionFastInverseSqrt(FusionVectorMagnitudeSquared(vector)); +#endif + return FusionVectorMultiplyScalar(vector, magnitudeReciprocal); +} + +//------------------------------------------------------------------------------ +// Inline functions - Quaternion operations + +/** + * @brief Returns the sum of two quaternions. + * @param quaternionA Quaternion A. + * @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; +} + +/** + * @brief Returns the multiplication of two quaternions. + * @param quaternionA Quaternion A (to be post-multiplied). + * @param quaternionB Quaternion B (to be pre-multiplied). + * @return Multiplication of two quaternions. + */ +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; +#undef A +#undef B +} + +/** + * @brief Returns the multiplication of a quaternion with a vector. This is a + * normal quaternion multiplication where the vector is treated a + * quaternion with a W element value of zero. The quaternion is post- + * multiplied by the vector. + * @param quaternion Quaternion. + * @param vector Vector. + * @return Multiplication of a quaternion with a 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; +#undef Q +#undef V +} + +/** + * @brief Returns the normalised quaternion. + * @param quaternion Quaternion. + * @return Normalised 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); +#else + 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; +#undef Q +} + +//------------------------------------------------------------------------------ +// Inline functions - Matrix operations + +/** + * @brief Returns the multiplication of a matrix with a vector. + * @param matrix Matrix. + * @param vector Vector. + * @return Multiplication of a matrix with a 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; +#undef R +} + +//------------------------------------------------------------------------------ +// Inline functions - Conversion operations + +/** + * @brief Converts a quaternion to a rotation matrix. + * @param quaternion Quaternion. + * @return Rotation matrix. + */ +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; +#undef Q +} + +/** + * @brief Converts a quaternion to ZYX Euler angles in degrees. + * @param quaternion Quaternion. + * @return Euler angles in degrees. + */ +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; +#undef Q +} + +#endif + +//------------------------------------------------------------------------------ +// End of file diff --git a/src/Fusion/FusionOffset.c b/src/Fusion/FusionOffset.c new file mode 100644 index 0000000..d4334c8 --- /dev/null +++ b/src/Fusion/FusionOffset.c @@ -0,0 +1,80 @@ +/** + * @file FusionOffset.c + * @author Seb Madgwick + * @brief Gyroscope offset correction algorithm for run-time calibration of the + * gyroscope offset. + */ + +//------------------------------------------------------------------------------ +// Includes + +#include "FusionOffset.h" +#include // fabsf + +//------------------------------------------------------------------------------ +// Definitions + +/** + * @brief Cutoff frequency in Hz. + */ +#define CUTOFF_FREQUENCY (0.02f) + +/** + * @brief Timeout in seconds. + */ +#define TIMEOUT (5) + +/** + * @brief Threshold in degrees per second. + */ +#define THRESHOLD (3.0f) + +//------------------------------------------------------------------------------ +// Functions + +/** + * @brief Initialises the gyroscope offset algorithm. + * @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; +} + +/** + * @brief Updates the gyroscope offset algorithm and returns the corrected + * gyroscope measurement. + * @param offset Gyroscope offset algorithm structure. + * @param gyroscope Gyroscope measurement in degrees per second. + * @return Corrected gyroscope measurement in degrees per second. + */ +FusionVector FusionOffsetUpdate(FusionOffset *const offset, FusionVector gyroscope) +{ + + // 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)); + return gyroscope; +} + +//------------------------------------------------------------------------------ +// End of file diff --git a/src/Fusion/FusionOffset.h b/src/Fusion/FusionOffset.h new file mode 100644 index 0000000..51ae4a8 --- /dev/null +++ b/src/Fusion/FusionOffset.h @@ -0,0 +1,40 @@ +/** + * @file FusionOffset.h + * @author Seb Madgwick + * @brief Gyroscope offset correction algorithm for run-time calibration of the + * gyroscope offset. + */ + +#ifndef FUSION_OFFSET_H +#define FUSION_OFFSET_H + +//------------------------------------------------------------------------------ +// Includes + +#include "FusionMath.h" + +//------------------------------------------------------------------------------ +// Definitions + +/** + * @brief Gyroscope offset algorithm structure. Structure members are used + * internally and must not be accessed by the application. + */ +typedef struct { + float filterCoefficient; + unsigned int timeout; + unsigned int timer; + FusionVector gyroscopeOffset; +} FusionOffset; + +//------------------------------------------------------------------------------ +// Function declarations + +void FusionOffsetInitialise(FusionOffset *const offset, const unsigned int sampleRate); + +FusionVector FusionOffsetUpdate(FusionOffset *const offset, FusionVector gyroscope); + +#endif + +//------------------------------------------------------------------------------ +// End of file diff --git a/src/GPSStatus.h b/src/GPSStatus.h new file mode 100644 index 0000000..12f302b --- /dev/null +++ b/src/GPSStatus.h @@ -0,0 +1,141 @@ +#pragma once +#include "NodeDB.h" +#include "Status.h" +#include "configuration.h" +#include + +namespace meshtastic +{ + +/// Describes the state of the GPS system. +class GPSStatus : public Status +{ + + 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 isPowerSaving = false; // Are we in power saving state + + meshtastic_Position p = meshtastic_Position_init_default; + + 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; + + // 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) { +#ifdef GPS_EXTRAVERBOSE + LOG_WARN("Using fixed latitude"); +#endif + 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) { +#ifdef GPS_EXTRAVERBOSE + LOG_WARN("Using fixed longitude"); +#endif + 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) { +#ifdef GPS_EXTRAVERBOSE + LOG_WARN("Using fixed altitude"); +#endif + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); + return node->position.altitude; + } else { + return p.altitude; + } + } + + uint32_t getDOP() const { return p.PDOP; } + + uint32_t getHeading() const { return p.ground_track; } + + uint32_t getNumSatellites() const { return p.sats_in_view; } + + bool matches(const GPSStatus *newStatus) const + { +#ifdef GPS_EXTRAVERBOSE + 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"); + } + + initialized = true; + hasLock = newStatus->hasLock; + isConnected = newStatus->isConnected; + + p = newStatus->p; + + if (isDirty) { + if (hasLock) { + // 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 + +extern meshtastic::GPSStatus *gpsStatus; diff --git a/src/GpioLogic.cpp b/src/GpioLogic.cpp new file mode 100644 index 0000000..ba01d48 --- /dev/null +++ b/src/GpioLogic.cpp @@ -0,0 +1,104 @@ +#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 GpioHwPin::set(bool value) +{ + // if (num == 3) LOG_DEBUG("Setting pin %d to %d", num, value); + pinMode(num, OUTPUT); + digitalWrite(num, value); +} + +GpioTransformer::GpioTransformer(GpioPin *outPin) : outPin(outPin) {} + +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; + + // 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 + + 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 + + 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; + + // 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(); +} + +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 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: + // LOG_DEBUG("Doing GPIO OR"); + newValue = (GpioVirtPin::PinState)(p1 || p2); + break; + case Xor: + newValue = (GpioVirtPin::PinState)(p1 != p2); + break; + default: + assert(false); + } + } + set(newValue); +} + +GpioSplitter::GpioSplitter(GpioPin *outPin1, GpioPin *outPin2) : outPin1(outPin1), outPin2(outPin2) {} \ No newline at end of file diff --git a/src/GpioLogic.h b/src/GpioLogic.h new file mode 100644 index 0000000..947d496 --- /dev/null +++ b/src/GpioLogic.h @@ -0,0 +1,160 @@ +#pragma once + +#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. + + Note: these classes are intended to be 99% inline for the common case so should have minimal impact on flash or RAM + requirements. +*/ + +/** + * A logical GPIO pin (not necessary raw hardware). + */ +class GpioPin +{ + public: + virtual void set(bool value) = 0; +}; + +/** + * A physical GPIO hw pin. + */ +class GpioHwPin : public GpioPin +{ + uint32_t num; + + public: + explicit GpioHwPin(uint32_t num) : num(num) {} + + void set(bool value); +}; + +class GpioTransformer; +class GpioNotTransformer; +class GpioBinaryTransformer; + +/** + * A virtual GPIO pin. + */ +class GpioVirtPin : public GpioPin +{ + friend class GpioBinaryTransformer; + friend class GpioUnaryTransformer; + + public: + enum PinState { On = true, Off = false, Unset = 2 }; + + void set(bool value); + PinState get() const { return value; } + + 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) + */ +class GpioTransformer +{ + public: + /** + * Update the output pin based on the current state of the input pin. + */ + virtual void update() = 0; + + protected: + GpioTransformer(GpioPin *outPin); + + void set(bool value); + + 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); + + protected: + friend class GpioVirtPin; + + /** + * Update the output pin based on the current state of the input pin. + */ + virtual void update(); + + 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) {} + + protected: + friend class GpioVirtPin; + + /** + * 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 +{ + + public: + enum Operation { And, Or, Xor }; + + GpioBinaryTransformer(GpioVirtPin *inPin1, GpioVirtPin *inPin2, GpioPin *outPin, Operation operation); + + protected: + friend class GpioVirtPin; + + /** + * Update the output pin based on the current state of the input pins. + */ + void update(); + + 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 +{ + + public: + GpioSplitter(GpioPin *outPin1, GpioPin *outPin2); + + void set(bool value) + { + outPin1->set(value); + outPin2->set(value); + } + + private: + GpioPin *outPin1; + GpioPin *outPin2; +}; \ No newline at end of file diff --git a/src/Led.cpp b/src/Led.cpp new file mode 100644 index 0000000..6406cd2 --- /dev/null +++ b/src/Led.cpp @@ -0,0 +1,66 @@ +#include "Led.h" +#include "PowerMon.h" +#include "main.h" +#include "power.h" + +GpioVirtPin ledForceOn, ledBlink; + +#if defined(LED_PIN) +// Most boards have a GPIO for LED control +static GpioHwPin ledRawHwPin(LED_PIN); +#else +static GpioVirtPin ledRawHwPin; // Dummy pin for no hardware +#endif + +#if LED_STATE_ON == 0 +static GpioVirtPin ledHwPin; +static GpioNotTransformer ledInverter(&ledHwPin, &ledRawHwPin); +#else +static GpioPin &ledHwPin = ledRawHwPin; +#endif + +#if defined(HAS_PMU) +/** + * 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); + } + } +} ledPmuHwPin; + +// In some cases we need to drive a PMU LED and a normal LED +static GpioSplitter ledFinalPin(&ledHwPin, &ledPmuHwPin); +#else +static GpioPin &ledFinalPin = ledHwPin; +#endif + +#ifdef USE_POWERMON +/** + * 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); + } +} monitoredLedPin; +#else +static GpioPin &monitoredLedPin = ledFinalPin; +#endif + +static GpioBinaryTransformer ledForcer(&ledForceOn, &ledBlink, &monitoredLedPin, GpioBinaryTransformer::Or); \ No newline at end of file diff --git a/src/Led.h b/src/Led.h new file mode 100644 index 0000000..68833e0 --- /dev/null +++ b/src/Led.h @@ -0,0 +1,7 @@ +#include "GpioLogic.h" +#include "configuration.h" + +/** + * ledForceOn and ledForceOff both override the normal ledBlinker behavior (which is controlled by main) + */ +extern GpioVirtPin ledForceOn, ledBlink; \ No newline at end of file diff --git a/src/NodeStatus.h b/src/NodeStatus.h new file mode 100644 index 0000000..60d1bdd --- /dev/null +++ b/src/NodeStatus.h @@ -0,0 +1,68 @@ +#pragma once +#include "Status.h" +#include "configuration.h" +#include + +namespace meshtastic +{ + +/// Describes the state of the NodeDB system. +class NodeStatus : public Status +{ + + private: + CallbackObserver statusObserver = + CallbackObserver(this, &NodeStatus::updateStatus); + + uint8_t numOnline = 0; + uint8_t numTotal = 0; + + uint8_t lastNumTotal = 0; + + public: + bool forceUpdate = false; + + NodeStatus() { statusType = STATUS_TYPE_NODE; } + NodeStatus(uint8_t numOnline, uint8_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); } + + uint8_t getNumOnline() const { return numOnline; } + + uint8_t getNumTotal() const { return numTotal; } + + uint8_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: %d online, %d total", numOnline, numTotal); + onNewStatus.notifyObservers(this); + } + return 0; + } +}; + +} // namespace meshtastic + +extern meshtastic::NodeStatus *nodeStatus; \ No newline at end of file diff --git a/src/Observer.cpp b/src/Observer.cpp new file mode 100644 index 0000000..bc938d9 --- /dev/null +++ b/src/Observer.cpp @@ -0,0 +1,2 @@ +#include "Observer.h" +#include "configuration.h" diff --git a/src/Observer.h b/src/Observer.h new file mode 100644 index 0000000..6e1ec44 --- /dev/null +++ b/src/Observer.h @@ -0,0 +1,106 @@ +#pragma once + +#include +#include + +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; + + public: + virtual ~Observer(); + + /// Stop watching the observable + void unobserve(Observable *o); + + /// Start watching a specified observable + void observe(Observable *o); + + 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; +}; + +/** + * An observer that calls an arbitrary method + */ +template class CallbackObserver : public Observer +{ + typedef int (Callback::*ObserverCallback)(T arg); + + Callback *objPtr; + ObserverCallback method; + + public: + CallbackObserver(Callback *_objPtr, ObserverCallback _method) : objPtr(_objPtr), method(_method) {} + + 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. + */ +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; + } + + private: + friend class Observer; + + // 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 void Observer::unobserve(Observable *o) +{ + o->removeObserver(this); + observables.remove(o); +} + +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 new file mode 100644 index 0000000..02a07e6 --- /dev/null +++ b/src/Power.cpp @@ -0,0 +1,1147 @@ +/** + * @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. + * + * 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/ + */ +#include "power.h" +#include "NodeDB.h" +#include "PowerFSM.h" +#include "Throttle.h" +#include "buzz/buzz.h" +#include "configuration.h" +#include "main.h" +#include "meshUtils.h" +#include "sleep.h" + +// Working USB detection for powered/charging states on the RAK platform +#ifdef NRF_APM +#include "nrfx_power.h" +#endif + +#if defined(DEBUG_HEAP_MQTT) && !MESHTASTIC_EXCLUDE_MQTT +#include "mqtt/MQTT.h" +#include "target_specific.h" +#if HAS_WIFI +#include +#endif + +#endif + +#ifndef DELAY_FOREVER +#define DELAY_FOREVER portMAX_DELAY +#endif + +#if defined(BATTERY_PIN) && defined(ARCH_ESP32) + +#ifndef BAT_MEASURE_ADC_UNIT // ADC1 is default +static const adc1_channel_t adc_channel = ADC_CHANNEL; +static const adc_unit_t unit = ADC_UNIT_1; +#else // ADC2 +static const adc2_channel_t adc_channel = ADC_CHANNEL; +static const adc_unit_t unit = ADC_UNIT_2; +RTC_NOINIT_ATTR uint64_t RTC_reg_b; + +#endif // BAT_MEASURE_ADC_UNIT + +esp_adc_cal_characteristics_t *adc_characs = (esp_adc_cal_characteristics_t *)calloc(1, sizeof(esp_adc_cal_characteristics_t)); +#ifndef ADC_ATTENUATION +static const adc_atten_t atten = ADC_ATTEN_DB_12; +#else +static const adc_atten_t atten = ADC_ATTENUATION; +#endif +#endif // BATTERY_PIN && ARCH_ESP32 + +#ifdef EXT_CHRG_DETECT +#ifndef EXT_CHRG_DETECT_MODE +static const uint8_t ext_chrg_detect_mode = INPUT; +#else +static const uint8_t ext_chrg_detect_mode = EXT_CHRG_DETECT_MODE; +#endif +#ifndef EXT_CHRG_DETECT_VALUE +static const uint8_t ext_chrg_detect_value = HIGH; +#else +static const uint8_t ext_chrg_detect_value = EXT_CHRG_DETECT_VALUE; +#endif +#endif + +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_PORTDUINO) +INA260Sensor ina260Sensor; +INA219Sensor ina219Sensor; +INA3221Sensor ina3221Sensor; +#endif + +#if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) +#include "modules/Telemetry/Sensor/MAX17048Sensor.h" +#include +extern std::pair nodeTelemetrySensorsMap[_meshtastic_TelemetrySensorType_MAX + 1]; +#if HAS_TELEMETRY && (!MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_POWER_TELEMETRY) +MAX17048Sensor max17048Sensor; +#endif +#endif + +#if HAS_RAKPROT && !defined(ARCH_PORTDUINO) +RAK9154Sensor rak9154Sensor; +#endif + +#ifdef HAS_PMU +XPowersLibInterface *PMU = NULL; +#else + +// 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; } + + /** + * 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; } + + virtual bool isVbusIn() { return false; } + virtual bool isCharging() { return false; } +}; +#endif + +bool pmu_irq = false; + +Power *power; + +using namespace meshtastic; + +#ifndef AREF_VOLTAGE +#if defined(ARCH_NRF52) +/* + * Internal Reference is +/-0.6V, with an adjustable gain of 1/6, 1/5, 1/4, + * 1/3, 1/2 or 1, meaning 3.6, 3.0, 2.4, 1.8, 1.2 or 0.6V for the ADC levels. + * + * External Reference is VDD/4, with an adjustable gain of 1, 2 or 4, meaning + * VDD/4, VDD/2 or VDD for the ADC levels. + * + * Default settings are internal reference with 1/6 gain (GND..3.6V ADC range) + */ +#define AREF_VOLTAGE 3.6 +#else +#define AREF_VOLTAGE 3.3 +#endif +#endif + +/** + * If this board has a battery level sensor, set this to a valid implementation + */ +static HasBatteryLevel *batteryLevel; // Default to NULL for no battery level sensor + +#ifdef BATTERY_PIN + +static void adcEnable() +{ +#ifdef ADC_CTRL // enable adc voltage divider when we need to read +#ifdef ADC_USE_PULLUP + pinMode(ADC_CTRL, INPUT_PULLUP); +#else + pinMode(ADC_CTRL, OUTPUT); + digitalWrite(ADC_CTRL, ADC_CTRL_ENABLED); +#endif + delay(10); +#endif +} + +static void adcDisable() +{ +#ifdef ADC_CTRL // disable adc voltage divider when we need to read +#ifdef ADC_USE_PULLUP + pinMode(ADC_CTRL, INPUT_PULLDOWN); +#else + digitalWrite(ADC_CTRL, !ADC_CTRL_ENABLED); +#endif +#endif +} + +#endif + +/** + * 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 + { +#if defined(HAS_RAKPROT) && !defined(ARCH_PORTDUINO) && !defined(HAS_PMU) + if (hasRAK()) { + return rak9154Sensor.getBusBatteryPercent(); + } +#endif + + float v = getBattVoltage(); + + 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 +#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; + } + } + return clamp((int)(battery_SOC), 0, 100); + } + + /** + * The raw voltage of the batteryin millivolts or NAN if unknown + */ + virtual uint16_t getBattVoltage() override + { + +#if defined(HAS_RAKPROT) && !defined(ARCH_PORTDUINO) && !defined(HAS_PMU) + if (hasRAK()) { + return getRAKVoltage(); + } +#endif + +#if HAS_TELEMETRY && !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !defined(HAS_PMU) && \ + !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + if (hasINA()) { + LOG_DEBUG("Using INA on I2C addr 0x%x for device battery voltage", config.power.device_battery_ina_address); + return getINAVoltage(); + } +#endif + +#ifndef ADC_MULTIPLIER +#define ADC_MULTIPLIER 2.0 +#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. +#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 (!Throttle::isWithinTimespanMs(last_read_time_ms, min_read_interval)) { + last_read_time_ms = millis(); + + uint32_t raw = 0; + float scaled = 0; + + adcEnable(); +#ifdef ARCH_ESP32 // ADC block for espressif platforms + 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; +#endif + 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 + } + + // 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() + { + + 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); + } +#else // ADC2 +#ifdef CONFIG_IDF_TARGET_ESP32S3 // ESP32S3 + // 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; + + // 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"); + } + } + +#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++; + } +#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; } +#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 +#ifdef HELTEC_CAPSULE_SENSOR_V3 + // 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 +#endif +#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 defined(HAS_RAKPROT) && !defined(ARCH_PORTDUINO) && !defined(HAS_PMU) + if (hasRAK()) { + return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse; + } +#endif +#ifdef EXT_CHRG_DETECT + return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value; +#else + return isBatteryConnect() && isVbusIn(); +#endif + } + + 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; + +#if defined(HAS_RAKPROT) + + uint16_t getRAKVoltage() { return rak9154Sensor.getBusVoltageMv(); } + + bool hasRAK() + { + if (!rak9154Sensor.isInitialized()) + return rak9154Sensor.runOnce() > 0; + return rak9154Sensor.isRunning(); + } +#endif + +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) + uint16_t getINAVoltage() + { + if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) { + return ina219Sensor.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; + } + + 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_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; +#ifdef DEBUG_HEAP + lastheap = memGet.getFreeHeap(); +#endif +} + +bool Power::analogInit() +{ +#ifdef EXT_PWR_DETECT +#ifdef HELTEC_CAPSULE_SENSOR_V3 + pinMode(EXT_PWR_DETECT, INPUT_PULLUP); +#else + pinMode(EXT_PWR_DETECT, INPUT); +#endif +#endif +#ifdef EXT_CHRG_DETECT + pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode); +#endif + +#ifdef BATTERY_PIN + LOG_DEBUG("Using analog input %d for battery level", BATTERY_PIN); + + // disable any internal pullups + pinMode(BATTERY_PIN, INPUT); + +#ifndef BATTERY_SENSE_RESOLUTION_BITS +#define BATTERY_SENSE_RESOLUTION_BITS 10 +#endif + +#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; +#else + 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); +#else // ADC2 + 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); +#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("ADCmod: ADC characterization based on Two Point values stored in eFuse"); + } else if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) { + LOG_INFO("ADCmod: ADC characterization 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("ADCmod: ADC Characterization based on Two Point values and fitting curve coefficients stored in eFuse"); + } +#endif + else { + LOG_INFO("ADCmod: ADC characterization based on default reference voltage"); + } +#endif // ARCH_ESP32 + +#ifdef ARCH_NRF52 +#ifdef VBAT_AR_INTERNAL + analogReference(VBAT_AR_INTERNAL); +#else + analogReference(AR_INTERNAL); // 3.6V +#endif +#endif // ARCH_NRF52 + +#ifndef ARCH_ESP32 + analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS); +#endif + + batteryLevel = &analogLevel; + return true; +#else + return false; +#endif +} + +/** + * Initializes the Power class. + * + * @return true if the setup was successful, false otherwise. + */ +bool Power::setup() +{ + // initialise one power sensor (only) + bool found = axpChipInit(); + if (!found) + found = lipoInit(); + if (!found) + found = analogInit(); + +#ifdef NRF_APM + found = true; +#endif + + enabled = found; + low_voltage_counter = 0; + + return found; +} + +void Power::shutdown() +{ + LOG_INFO("Shutting down"); + +#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040) +#ifdef PIN_LED1 + ledOff(PIN_LED1); +#endif +#ifdef PIN_LED2 + ledOff(PIN_LED2); +#endif +#ifdef PIN_LED3 + ledOff(PIN_LED3); +#endif + doDeepSleep(DELAY_FOREVER, false); +#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 isCharging = OptUnknown; + + if (batteryLevel) { + hasBattery = batteryLevel->isBatteryConnect() ? OptTrue : OptFalse; + usbPowered = batteryLevel->isVbusIn() ? OptTrue : OptFalse; + isCharging = 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 circuite 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) + isCharging = usbPowered = OptFalse; + // If changed to CONNECTED / READY + else + isCharging = usbPowered = OptTrue; + +#endif + + // Notify any status instances that are observing us + const PowerStatus powerStatus2 = PowerStatus(hasBattery, usbPowered, isCharging, batteryVoltageMv, batteryChargePercent); + LOG_DEBUG("Battery: usbPower=%d, isCharging=%d, batMv=%d, batPct=%d", powerStatus2.getHasUSB(), powerStatus2.getIsCharging(), + powerStatus2.getBatteryVoltageMv(), powerStatus2.getBatteryChargePercent()); + newStatus.notifyObservers(&powerStatus2); +#ifdef DEBUG_HEAP + if (lastheap != memGet.getFreeHeap()) { + std::string threadlist = "Threads running:"; + int running = 0; + for (int i = 0; i < MAX_THREADS; i++) { + auto thread = concurrency::mainController.get(i); + if ((thread != nullptr) && (thread->enabled)) { + threadlist += vformat(" %s", thread->ThreadName.c_str()); + running++; + } + } + LOG_DEBUG(threadlist.c_str()); + LOG_DEBUG("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]); + + auto newHeap = memGet.getFreeHeap(); + std::string heapTopic = + (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh") + std::string("/2/heap/") + std::string(mac); + std::string heapString = std::to_string(newHeap); + mqtt->pubSub.publish(heapTopic.c_str(), heapString.c_str(), false); + auto wifiRSSI = WiFi.RSSI(); + std::string wifiTopic = + (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh") + std::string("/2/wifi/") + std::string(mac); + std::string wifiString = std::to_string(wifiRSSI); + mqtt->pubSub.publish(wifiTopic.c_str(), wifiString.c_str(), 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) { +#ifdef ARCH_NRF52 + // We can't trigger deep sleep on NRF52, it's freezing the board + LOG_DEBUG("Low voltage detected, but not triggering deep sleep"); +#else + LOG_INFO("Low voltage detected, triggering deep sleep"); + powerFSM.trigger(EVENT_LOW_BATTERY); +#endif + } + } else { + low_voltage_counter = 0; + } + } +} + +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) { + + PMU->getIrqStatus(); + + 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); + } + + /* + 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"); + } + */ +#ifndef T_WATCH_S3 // FIXME - why is this triggering on the T-Watch S3? + if (PMU->isPekeyLongPressIrq()) { + LOG_DEBUG("PEK long button press"); + screen->setOn(false); + } +#endif + + 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; +} + +/** + * Init the power manager chip + * + * 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 + * + */ +bool Power::axpChipInit() +{ + +#ifdef HAS_PMU + + TwoWire *w = NULL; + + // Use macro to distinguish which wire is used by PMU +#ifdef PMU_USE_WIRE1 + w = &Wire1; +#else + 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("Failed to find AXP2101 power management"); + delete PMU; + PMU = NULL; + } else { + LOG_INFO("AXP2101 PMU init succeeded, using AXP2101 PMU"); + } + } + + if (!PMU) { + PMU = new XPowersAXP192(*w); + if (!PMU->init()) { + LOG_WARN("Failed to find AXP192 power management"); + delete PMU; + PMU = NULL; + } else { + LOG_INFO("AXP192 PMU init succeeded, using AXP192 PMU"); + } + } + + 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); +#endif + return false; + } + + batteryLevel = PMU; + + if (PMU->getChipModel() == XPOWERS_AXP192) { + + // 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); + + // 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); + + // disable not use channel + PMU->disablePowerOutput(XPOWERS_DCDC2); + + // disable all axp chip interrupt + PMU->disableIRQ(XPOWERS_AXP192_ALL_IRQ); + + // 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) { + + /*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); + + // 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); + + // 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); + + // 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); + + // 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); + +#ifdef T_WATCH_S3 + // DRV2605 power channel + PMU->setPowerChannelVoltage(XPOWERS_BLDO2, 3300); + PMU->enablePowerOutput(XPOWERS_BLDO2); +#endif + + // 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); + } + + 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. +// +// 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); +#endif + +#ifdef PMU_IRQ + 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; + } + + 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); + + PMU->clearIrqStatus(); +#endif /*PMU_IRQ*/ + + readPowerStatus(); + + pmu_found = true; + + return pmu_found; + +#else + return false; +#endif +} + +#if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) + +/** + * Wrapper class for an I2C MAX17048 Lipo battery sensor. + */ +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; + } + + /** + * 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(); } + + /** + * 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 the battery is currently charging + */ + virtual bool isCharging() override { return max17048->isBatteryCharging(); } +}; + +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; +} + +#else +/** + * The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel + */ +bool Power::lipoInit() +{ + return false; +} +#endif \ No newline at end of file diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp new file mode 100644 index 0000000..35ef262 --- /dev/null +++ b/src/PowerFSM.cpp @@ -0,0 +1,402 @@ +/** + * @file PowerFSM.cpp + * @brief Implements the finite state machine for power management. + * + * This file contains the implementation of the finite state machine (FSM) for power management. + * The FSM controls the power states of the device, including SDS (shallow deep sleep), LS (light sleep), + * NB (normal mode), and POWER (powered mode). The FSM also handles transitions between states and + * actions to be taken upon entering or exiting each state. + */ +#include "PowerFSM.h" +#include "Default.h" +#include "Led.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "PowerMon.h" +#include "configuration.h" +#include "graphics/Screen.h" +#include "main.h" +#include "sleep.h" +#include "target_specific.h" + +#ifndef SLEEP_TIME +#define SLEEP_TIME 30 +#endif +#if EXCLUDE_POWER_FSM +FakeFsm powerFSM; +void PowerFSM_setup(){}; +#else +/// Should we behave as if we have AC power now? +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; +#endif + + 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; + + /* 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. + + 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_DEBUG("Enter 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); +} + +extern Power *power; + +static void shutdownEnter() +{ + LOG_DEBUG("Enter state: SHUTDOWN"); + power->shutdown(); +} + +#include "error.h" + +static uint32_t secsSlept; + +static void lsEnter() +{ + LOG_INFO("lsEnter begin, ls_secs=%u", config.power.ls_secs); + screen->setOn(false); + secsSlept = 0; // How long have we been sleeping this time + + // LOG_INFO("lsEnter end"); +} + +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; + + 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 + + ledBlink.set(true); // briefly turn on led + wakeCause2 = doLightSleep(100); // leave led on for 1ms + + secsSlept += sleepTime; + // LOG_INFO("sleeping, flash led!"); + 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) + +#ifdef BUTTON_PIN + bool pressed = !digitalRead(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN); +#else + 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; + } + } else { + // 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, servicing loop()"); + powerFSM.trigger(EVENT_WAKE_TIMER); + } +#endif +} + +static void lsExit() +{ + LOG_INFO("Exit state: LS"); +} + +static void nbEnter() +{ + LOG_DEBUG("Enter state: NB"); + 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() +{ + setBluetoothEnable(true); + screen->setOn(false); +} + +static void serialEnter() +{ + LOG_DEBUG("Enter state: SERIAL"); + setBluetoothEnable(false); + screen->setOn(true); + screen->print("Serial connected\n"); +} + +static void serialExit() +{ + // Turn bluetooth back on when we leave serial stream API + setBluetoothEnable(true); + screen->print("Serial disconnected\n"); +} + +static void powerEnter() +{ + // LOG_DEBUG("Enter state: POWER"); + if (!isPowered()) { + // If we got here, we are in the wrong state - we should be in powered, let that state ahndle things + LOG_INFO("Loss of power in Powered"); + powerFSM.trigger(EVENT_POWER_DISCONNECTED); + } else { + screen->setOn(true); + setBluetoothEnable(true); + // within enter() the function getState() returns the state we came from + + // Mothballed: print change of power-state to device screen + /* if (strcmp(powerFSM.getState()->name, "BOOT") != 0 && strcmp(powerFSM.getState()->name, "POWER") != 0 && + strcmp(powerFSM.getState()->name, "DARK") != 0) { + screen->print("Powered...\n"); + }*/ + } +} + +static void powerIdle() +{ + 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() +{ + screen->setOn(true); + setBluetoothEnable(true); + + // Mothballed: print change of power-state to device screen + /*if (!isPowered()) + screen->print("Unpowered...\n");*/ +} + +static void onEnter() +{ + LOG_DEBUG("Enter state: ON"); + screen->setOn(true); + setBluetoothEnable(true); +} + +static void onIdle() +{ + if (isPowered()) { + // If we got here, we are in the wrong state - we should be in powered, let that state ahndle things + powerFSM.trigger(EVENT_POWER_CONNECTED); + } +} + +static void screenPress() +{ + screen->onPress(); +} + +static void bootEnter() +{ + LOG_DEBUG("Enter state: BOOT"); +} + +State stateSHUTDOWN(shutdownEnter, NULL, NULL, "SHUTDOWN"); +State stateSDS(sdsEnter, NULL, NULL, "SDS"); +State stateLS(lsEnter, lsIdle, lsExit, "LS"); +State stateNB(nbEnter, NULL, NULL, "NB"); +State stateDARK(darkEnter, NULL, NULL, "DARK"); +State stateSERIAL(serialEnter, NULL, serialExit, "SERIAL"); +State stateBOOT(bootEnter, NULL, NULL, "BOOT"); +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 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 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"); + + // 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"); +#else // Don't go into a no-bluetooth state on low power platforms + 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"); + + // 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, screenPress, "Press"); + powerFSM.add_transition(&stateON, &stateON, EVENT_PRESS, screenPress, "Press"); // reenter On to restart our timers + powerFSM.add_transition(&stateSERIAL, &stateSERIAL, EVENT_PRESS, screenPress, + "Press"); // Allow button to work while in serial API + + // Handle critically low power battery by forcing deep sleep + powerFSM.add_transition(&stateBOOT, &stateSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); + powerFSM.add_transition(&stateLS, &stateSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); + powerFSM.add_transition(&stateNB, &stateSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); + powerFSM.add_transition(&stateDARK, &stateSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); + powerFSM.add_transition(&stateON, &stateSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); + powerFSM.add_transition(&stateSERIAL, &stateSDS, 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"); + + // 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"); + + // 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 latest node when we get a new node db update + powerFSM.add_transition(&stateNB, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); + powerFSM.add_transition(&stateDARK, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); + powerFSM.add_transition(&stateON, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); + + // 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 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"); + + // 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"); + +#ifdef USE_EINK + // 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"); + } + +// 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. Sleep will be initiatiated through the + // modules + if ((isRouter || config.power.is_power_saving) && !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"); + } +#else + // 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 +} +#endif \ No newline at end of file diff --git a/src/PowerFSM.h b/src/PowerFSM.h new file mode 100644 index 0000000..13dfdc4 --- /dev/null +++ b/src/PowerFSM.h @@ -0,0 +1,51 @@ +#pragma once + +#include "configuration.h" + +// See sw-design.md for documentation + +#define EVENT_PRESS 1 +#define EVENT_WAKE_TIMER 2 +// #define EVENT_RECEIVED_PACKET 3 +#define EVENT_PACKET_FOR_PHONE 4 +#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 // 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 +#define EVENT_SERIAL_DISCONNECTED 12 +#define EVENT_POWER_CONNECTED 13 +#define EVENT_POWER_DISCONNECTED 14 +#define EVENT_FIRMWARE_UPDATE 15 // We just received a new firmware update packet from the phone +#define EVENT_SHUTDOWN 16 // force a full shutdown now (not just sleep) +#define EVENT_INPUT 17 // input broker wants something, we need to wake up and enable screen + +#if 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; }; + + private: + bool serialConnected = false; +}; +extern FakeFsm powerFSM; +void PowerFSM_setup(); + +#else +#include +extern Fsm powerFSM; +extern State stateON, statePOWER, stateSERIAL, stateDARK; + +void PowerFSM_setup(); +#endif \ No newline at end of file diff --git a/src/PowerFSMThread.h b/src/PowerFSMThread.h new file mode 100644 index 0000000..c842f45 --- /dev/null +++ b/src/PowerFSMThread.h @@ -0,0 +1,45 @@ +#include "Default.h" +#include "NodeDB.h" +#include "PowerFSM.h" +#include "concurrency/OSThread.h" +#include "configuration.h" +#include "main.h" +#include "power.h" + +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") {} + + protected: + int32_t runOnce() override + { +#if !EXCLUDE_POWER_FSM + 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 (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 new file mode 100644 index 0000000..38740b6 --- /dev/null +++ b/src/PowerMon.cpp @@ -0,0 +1,47 @@ +#include "PowerMon.h" +#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); +} + +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); + } +#endif +} + +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); + } +#endif +} + +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); +#endif +} + +PowerMon *powerMon; + +void powerMonInit() +{ + powerMon = new PowerMon(); +} \ No newline at end of file diff --git a/src/PowerMon.h b/src/PowerMon.h new file mode 100644 index 0000000..a19a6d0 --- /dev/null +++ b/src/PowerMon.h @@ -0,0 +1,44 @@ +#pragma once +#include "configuration.h" + +#include "meshtastic/powermon.pb.h" + +#ifndef MESHTASTIC_EXCLUDE_POWERMON +#define USE_POWERMON // FIXME turn this only for certain builds +#endif + +/** + * The singleton class for monitoring power consumption of device + * subsystems/modes. + * + * For more information see the PowerMon docs. + */ +class PowerMon +{ + uint64_t states = 0UL; + + friend class PowerStressModule; + + /** + * If stress testing we always want all events logged + */ + bool force_enabled = false; + + 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 = ""); + + 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); +}; + +extern PowerMon *powerMon; + +void powerMonInit(); \ No newline at end of file diff --git a/src/PowerStatus.h b/src/PowerStatus.h new file mode 100644 index 0000000..fe4543e --- /dev/null +++ b/src/PowerStatus.h @@ -0,0 +1,103 @@ +#pragma once +#include "Status.h" +#include "configuration.h" +#include + +namespace meshtastic +{ + +/** + * A boolean where we have a third state of Unknown + */ +enum OptionalBool { OptFalse = 0, OptTrue = 1, OptUnknown = 2 }; + +/// Describes the state of the Power system. +class PowerStatus : public Status +{ + + 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; + + 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; + { + 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; + } +}; + +} // namespace meshtastic + +extern meshtastic::PowerStatus *powerStatus; diff --git a/src/RF95Configuration.h b/src/RF95Configuration.h new file mode 100644 index 0000000..5c52581 --- /dev/null +++ b/src/RF95Configuration.h @@ -0,0 +1,6 @@ +// TODO refactor this out with better radio configuration system +#ifdef USE_RF95 +#define RF95_RESET LORA_RESET +#define RF95_IRQ LORA_DIO0 // on SX1262 version this is a no connect DIO0 +#define RF95_DIO1 LORA_DIO1 // Note: not really used for RF95, but used for pure SX127x +#endif diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp new file mode 100644 index 0000000..57f5301 --- /dev/null +++ b/src/RedirectablePrint.cpp @@ -0,0 +1,396 @@ +#include "RedirectablePrint.h" +#include "NodeDB.h" +#include "RTC.h" +#include "concurrency/OSThread.h" +#include "configuration.h" +#include "main.h" +#include "mesh/generated/meshtastic/mesh.pb.h" +#include +#include +#include +#include +#include +#include + +#ifdef ARCH_PORTDUINO +#include "platform/portduino/PortduinoGlue.h" +#endif + +#if HAS_NETWORKING +extern Syslog syslog; +#endif +void RedirectablePrint::rpInit() +{ +#ifdef HAS_FREE_RTOS + inDebugPrint = xSemaphoreCreateMutexStatic(&this->_MutexStorageSpace); +#endif +} + +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 +#ifdef USE_SEGGER + 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); + + 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; +#if ENABLE_JSON_LOGGING || ARCH_PORTDUINO + static char printBuf[512]; +#else + static char printBuf[160]; +#endif + +#ifdef ARCH_PORTDUINO + bool color = !settingsMap[ascii_logs]; +#else + bool color = true; +#endif + + 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 (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", 6); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) + Print::write("\u001b[32m", 6); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) + Print::write("\u001b[33m", 6); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_ERROR) == 0) + Print::write("\u001b[31m", 6); + } + len = Print::write(printBuf, len); + if (color && logLevel != nullptr) { + Print::write("\u001b[0m", 5); + } + return len; +} + +void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, va_list arg) +{ + size_t r = 0; + +#ifdef ARCH_PORTDUINO + bool color = !settingsMap[ascii_logs]; +#else + bool color = true; +#endif + + // include the header + if (color) { + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) + Print::write("\u001b[34m", 6); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) + Print::write("\u001b[32m", 6); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) + Print::write("\u001b[33m", 6); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_ERROR) == 0) + Print::write("\u001b[31m", 6); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) + Print::write("\u001b[35m", 6); + } + + 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); +#else + printf("%s ", logLevel); + if (color) { + printf("\u001b[0m"); + } + printf("| %02d:%02d:%02d %u ", hour, min, sec, millis() / 1000); +#endif + } else { +#ifdef ARCH_PORTDUINO + ::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); +#endif + } + auto thread = concurrency::OSThread::currentThread; + if (thread) { + print("["); + // printf("%p ", thread); + // assert(thread->ThreadName.length()); + print(thread->ThreadName); + print("] "); + } + 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 +} + +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 + 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 (settingsStrings[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 (settingsMap[logoutputlevel] < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { + delete[] newFormat; + return; + } + } + if (settingsMap[logoutputlevel] < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { + delete[] newFormat; + return; + } else if (settingsMap[logoutputlevel] < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) { + delete[] newFormat; + return; + } else if (settingsMap[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 = 1, iy = 52; + 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; + if (i < 256) + log(logLevel, " "); + log(logLevel, "%02x", index); + log(logLevel, "."); + 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 new file mode 100644 index 0000000..45b62b7 --- /dev/null +++ b/src/RedirectablePrint.h @@ -0,0 +1,59 @@ +#pragma once + +#include "../freertosinc.h" +#include "mesh/generated/meshtastic/mesh.pb.h" +#include +#include +#include + +/** + * A Printable that can be switched to squirt its bytes to a different sink. + * 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; + +#ifdef HAS_FREE_RTOS + SemaphoreHandle_t inDebugPrint = nullptr; + StaticSemaphore_t _MutexStorageSpace; +#else + volatile bool inDebugPrint = false; +#endif + public: + explicit RedirectablePrint(Print *_dest) : dest(_dest) {} + + /** + * Set a new destination + */ + void rpInit(); + void setDestination(Print *dest); + + 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))); + + /** 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); + + 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); + + 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 new file mode 100644 index 0000000..13fa556 --- /dev/null +++ b/src/SPILock.cpp @@ -0,0 +1,12 @@ +#include "SPILock.h" +#include "configuration.h" +#include +#include + +concurrency::Lock *spiLock; + +void initSPI() +{ + assert(!spiLock); + spiLock = new concurrency::Lock(); +} \ No newline at end of file diff --git a/src/SPILock.h b/src/SPILock.h new file mode 100644 index 0000000..8a4bd5d --- /dev/null +++ b/src/SPILock.h @@ -0,0 +1,12 @@ +#pragma once + +#include "../concurrency/LockGuard.h" + +/** + * Used to provide mutual exclusion for access to the SPI bus. Usage: + * concurrency::LockGuard g(spiLock); + */ +extern concurrency::Lock *spiLock; + +/** Setup SPI access and create the spiLock lock. */ +void initSPI(); \ No newline at end of file diff --git a/src/SafeFile.cpp b/src/SafeFile.cpp new file mode 100644 index 0000000..c76ff80 --- /dev/null +++ b/src/SafeFile.cpp @@ -0,0 +1,105 @@ +#include "SafeFile.h" + +#ifdef FSCom + +// Only way to work on both esp32 and nrf52 +static File openFile(const char *filename, bool fullAtomic) +{ + if (!fullAtomic) + FSCom.remove(filename); // Nuke the old file to make space (ignore if it !exists) + + String filenameTmp = filename; + filenameTmp += ".tmp"; + + // clear any previous LFS errors + lfs_assert_failed = false; + + 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); +} + +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) +} + +/** + * Atomically close the file (deleting any old versions) and readback the contents to confirm the hash matches + * + * @return false for failure + */ +bool SafeFile::close() +{ + if (!f) + return false; + + f.close(); + if (!testReadback()) + return false; + + // 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; + } + + return true; +} + +/// Read our (closed) tempfile back in and compare the hash +bool SafeFile::testReadback() +{ + bool lfs_failed = lfs_assert_failed; + lfs_assert_failed = 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(); + + if (test_hash != hash) { + LOG_ERROR("Readback failed hash mismatch"); + return false; + } + + return !lfs_failed; +} + +#endif \ No newline at end of file diff --git a/src/SafeFile.h b/src/SafeFile.h new file mode 100644 index 0000000..61361d3 --- /dev/null +++ b/src/SafeFile.h @@ -0,0 +1,49 @@ +#pragma once + +#include "FSCommon.h" +#include "configuration.h" + +#ifdef FSCom + +/** + * 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. + * + * 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. + */ +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); + + /** + * 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(); + + 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 new file mode 100644 index 0000000..68c4198 --- /dev/null +++ b/src/SerialConsole.cpp @@ -0,0 +1,107 @@ +#include "SerialConsole.h" +#include "Default.h" +#include "NodeDB.h" +#include "PowerFSM.h" +#include "Throttle.h" +#include "configuration.h" +#include "time.h" + +#ifdef RP2040_SLOW_CLOCK +#define Port Serial2 +#else +#ifdef USER_DEBUG_PORT // change by WayenWeng +#define Port USER_DEBUG_PORT +#else +#define Port Serial +#endif +#endif +// Defaulting to the formerly removed phone_timeout_secs value of 15 minutes +#define SERIAL_CONNECTION_TIMEOUT (15 * 60) * 1000UL + +SerialConsole *console; + +void consoleInit() +{ + new SerialConsole(); // Must be dynamically allocated because we are now inheriting from thread + 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(); +} + +SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), concurrency::OSThread("SerialConsole") +{ + 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); +#endif + 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; + } + } +#endif +#if !ARCH_PORTDUINO + emitRebooted(); +#endif +} + +int32_t SerialConsole::runOnce() +{ + return runOncePart(); +} + +void SerialConsole::flush() +{ + Port.flush(); +} + +// 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; + + 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); +} \ No newline at end of file diff --git a/src/SerialConsole.h b/src/SerialConsole.h new file mode 100644 index 0000000..f1e636c --- /dev/null +++ b/src/SerialConsole.h @@ -0,0 +1,48 @@ +#pragma once + +#include "RedirectablePrint.h" +#include "StreamAPI.h" +/** + * 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; + + 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; + + 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; + + void flush(); + + protected: + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() 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); +}; + +// A simple wrapper to allow non class aware code write to the console +void consolePrintf(const char *format, ...); +void consoleInit(); + +extern SerialConsole *console; \ No newline at end of file diff --git a/src/Status.h b/src/Status.h new file mode 100644 index 0000000..65f3a25 --- /dev/null +++ b/src/Status.h @@ -0,0 +1,56 @@ +#pragma once + +#include "Observer.h" + +// Constants for the various status types, so we can tell subclass instances apart +#define STATUS_TYPE_BASE 0 +#define STATUS_TYPE_POWER 1 +#define STATUS_TYPE_GPS 2 +#define STATUS_TYPE_NODE 3 + +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; + + public: + // Allows us to generate observable events + Observable onNewStatus; + + // Enable polymorphism ? + virtual ~Status() = default; + + Status() + { + if (!statusType) { + statusType = STATUS_TYPE_BASE; + } + } + + // 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); } + + // 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; } + + int getStatusType() const { return statusType; } + + // 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 new file mode 100644 index 0000000..7478deb --- /dev/null +++ b/src/airtime.cpp @@ -0,0 +1,221 @@ +#include "airtime.h" +#include "NodeDB.h" +#include "configuration.h" + +AirTime *airTime = NULL; + +// Don't read out of this directly. Use the helper functions. + +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) +{ + + if (reportType == TX_LOG) { + LOG_DEBUG("Packet transmitted : %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 received : %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 received (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("Rotating 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]; + // LOG_DEBUG("ChanUtilArray %u %u", i, 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("Channel utilization is >%d percent. Skipping this opportunity to 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 utilization is >%f percent. Skipping this opportunity to 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; + } + + return MINUTES_IN_HOUR; +} + +AirTime::AirTime() : concurrency::OSThread("AirTime"), airtimes({}) {} + +int32_t AirTime::runOnce() +{ + secSinceBoot++; + + uint8_t utilPeriod = this->getPeriodUtilMinute(); + uint8_t utilPeriodTX = this->getPeriodUtilHour(); + + 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; + } + } + /* + LOG_DEBUG("utilPeriodTX %d TX Airtime %3.2f%", utilPeriodTX, airTime->utilizationTXPercent()); + for (uint32_t i = 0; i < MINUTES_IN_HOUR; i++) { + LOG_DEBUG( + "%d,", this->utilizationTX[i] + ); + } + LOG_DEBUG(""); + */ + return (1000 * 1); +} \ No newline at end of file diff --git a/src/airtime.h b/src/airtime.h new file mode 100644 index 0000000..3ed7b6d --- /dev/null +++ b/src/airtime.h @@ -0,0 +1,89 @@ +#pragma once + +#include "MeshRadio.h" +#include "concurrency/OSThread.h" +#include "configuration.h" +#include +#include + +/* + TX_LOG - Time on air this device has transmitted + + RX_LOG - Time on air used by valid and routable mesh packets, does not include + TX air time + + RX_ALL_LOG - Time of all received lora packets. This includes packets that are not + for meshtastic devices. Does not include TX air time. + + Example analytics: + + TX_LOG + RX_LOG = Total air time for a particular meshtastic channel. + + TX_LOG + RX_LOG = Total air time for a particular meshtastic channel, including + other lora radios. + + RX_ALL_LOG - RX_LOG = Other lora radios on our frequency channel. +*/ + +#define CHANNEL_UTILIZATION_PERIODS 6 +#define SECONDS_PER_PERIOD 3600 +#define PERIODS_TO_LOG 8 +#define MINUTES_IN_HOUR 60 +#define SECONDS_IN_MINUTE 60 +#define MS_IN_MINUTE (SECONDS_IN_MINUTE * 1000) +#define MS_IN_HOUR (MINUTES_IN_HOUR * SECONDS_IN_MINUTE * 1000) + +enum reportTypes { TX_LOG, RX_LOG, RX_ALL_LOG }; + +void logAirtime(reportTypes reportType, uint32_t airtime_ms); + +uint32_t *airtimeReport(reportTypes reportType); + +class AirTime : private concurrency::OSThread +{ + + public: + AirTime(); + + 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}; + + 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 + + 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(); + + protected: + virtual int32_t runOnce() override; +}; + +extern AirTime *airTime; diff --git a/src/buzz/buzz.cpp b/src/buzz/buzz.cpp new file mode 100644 index 0000000..8db9602 --- /dev/null +++ b/src/buzz/buzz.cpp @@ -0,0 +1,80 @@ +#include "buzz.h" +#include "NodeDB.h" +#include "configuration.h" + +#if !defined(ARCH_ESP32) && !defined(ARCH_RP2040) && !defined(ARCH_PORTDUINO) +#include "Tone.h" +#endif + +#if !defined(ARCH_PORTDUINO) +extern "C" void delay(uint32_t dwMs); +#endif + +struct ToneDuration { + int frequency_khz; + int duration_ms; +}; + +// Some common frequencies. +#define NOTE_C3 131 +#define NOTE_CS3 139 +#define NOTE_D3 147 +#define NOTE_DS3 156 +#define NOTE_E3 165 +#define NOTE_F3 175 +#define NOTE_FS3 185 +#define NOTE_G3 196 +#define NOTE_GS3 208 +#define NOTE_A3 220 +#define NOTE_AS3 233 +#define NOTE_B3 247 +#define NOTE_CS4 277 + +const int DURATION_1_8 = 125; // 1/8 note +const int DURATION_1_4 = 250; // 1/4 note + +void playTones(const ToneDuration *tone_durations, int size) +{ +#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 playBeep() +{ + ToneDuration melody[] = {{NOTE_B3, DURATION_1_4}}; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +} + +void playGPSEnableBeep() +{ + ToneDuration melody[] = {{NOTE_C3, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_CS4, DURATION_1_4}}; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +} + +void playGPSDisableBeep() +{ + ToneDuration melody[] = {{NOTE_CS4, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_C3, 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)); +} diff --git a/src/buzz/buzz.h b/src/buzz/buzz.h new file mode 100644 index 0000000..c52c302 --- /dev/null +++ b/src/buzz/buzz.h @@ -0,0 +1,7 @@ +#pragma once + +void playBeep(); +void playStartMelody(); +void playShutdownMelody(); +void playGPSEnableBeep(); +void playGPSDisableBeep(); \ No newline at end of file diff --git a/src/commands.h b/src/commands.h new file mode 100644 index 0000000..f2b7830 --- /dev/null +++ b/src/commands.h @@ -0,0 +1,18 @@ +/** + * @brief This class enables on the fly software and hardware setup. + * It will contain all command messages to change internal settings. + */ + +enum class Cmd { + INVALID, + SET_ON, + SET_OFF, + ON_PRESS, + START_ALERT_FRAME, + STOP_ALERT_FRAME, + START_FIRMWARE_UPDATE_SCREEN, + STOP_BOOT_SCREEN, + PRINT, + SHOW_PREV_FRAME, + SHOW_NEXT_FRAME +}; \ No newline at end of file diff --git a/src/concurrency/BinarySemaphoreFreeRTOS.cpp b/src/concurrency/BinarySemaphoreFreeRTOS.cpp new file mode 100644 index 0000000..36e55ea --- /dev/null +++ b/src/concurrency/BinarySemaphoreFreeRTOS.cpp @@ -0,0 +1,40 @@ +#include "concurrency/BinarySemaphoreFreeRTOS.h" +#include "configuration.h" +#include + +#ifdef HAS_FREE_RTOS + +namespace concurrency +{ + +BinarySemaphoreFreeRTOS::BinarySemaphoreFreeRTOS() : semaphore(xSemaphoreCreateBinary()) +{ + assert(semaphore); +} + +BinarySemaphoreFreeRTOS::~BinarySemaphoreFreeRTOS() +{ + vSemaphoreDelete(semaphore); +} + +/** + * Returns false if we were interrupted + */ +bool BinarySemaphoreFreeRTOS::take(uint32_t msec) +{ + return xSemaphoreTake(semaphore, pdMS_TO_TICKS(msec)); +} + +void BinarySemaphoreFreeRTOS::give() +{ + xSemaphoreGive(semaphore); +} + +IRAM_ATTR void BinarySemaphoreFreeRTOS::giveFromISR(BaseType_t *pxHigherPriorityTaskWoken) +{ + xSemaphoreGiveFromISR(semaphore, pxHigherPriorityTaskWoken); +} + +} // namespace concurrency + +#endif diff --git a/src/concurrency/BinarySemaphoreFreeRTOS.h b/src/concurrency/BinarySemaphoreFreeRTOS.h new file mode 100644 index 0000000..2883151 --- /dev/null +++ b/src/concurrency/BinarySemaphoreFreeRTOS.h @@ -0,0 +1,30 @@ +#pragma once + +#include "../freertosinc.h" + +namespace concurrency +{ + +#ifdef HAS_FREE_RTOS + +class BinarySemaphoreFreeRTOS +{ + SemaphoreHandle_t semaphore; + + public: + BinarySemaphoreFreeRTOS(); + ~BinarySemaphoreFreeRTOS(); + + /** + * Returns false if we timed out + */ + bool take(uint32_t msec); + + void give(); + + void giveFromISR(BaseType_t *pxHigherPriorityTaskWoken); +}; + +#endif + +} // namespace concurrency \ No newline at end of file diff --git a/src/concurrency/BinarySemaphorePosix.cpp b/src/concurrency/BinarySemaphorePosix.cpp new file mode 100644 index 0000000..dc49a48 --- /dev/null +++ b/src/concurrency/BinarySemaphorePosix.cpp @@ -0,0 +1,28 @@ +#include "concurrency/BinarySemaphorePosix.h" +#include "configuration.h" + +#ifndef HAS_FREE_RTOS + +namespace concurrency +{ + +BinarySemaphorePosix::BinarySemaphorePosix() {} + +BinarySemaphorePosix::~BinarySemaphorePosix() {} + +/** + * Returns false if we timed out + */ +bool BinarySemaphorePosix::take(uint32_t msec) +{ + delay(msec); // FIXME + return false; +} + +void BinarySemaphorePosix::give() {} + +IRAM_ATTR void BinarySemaphorePosix::giveFromISR(BaseType_t *pxHigherPriorityTaskWoken) {} + +} // namespace concurrency + +#endif \ No newline at end of file diff --git a/src/concurrency/BinarySemaphorePosix.h b/src/concurrency/BinarySemaphorePosix.h new file mode 100644 index 0000000..475b298 --- /dev/null +++ b/src/concurrency/BinarySemaphorePosix.h @@ -0,0 +1,30 @@ +#pragma once + +#include "../freertosinc.h" + +namespace concurrency +{ + +#ifndef HAS_FREE_RTOS + +class BinarySemaphorePosix +{ + // SemaphoreHandle_t semaphore; + + public: + BinarySemaphorePosix(); + ~BinarySemaphorePosix(); + + /** + * Returns false if we timed out + */ + bool take(uint32_t msec); + + void give(); + + void giveFromISR(BaseType_t *pxHigherPriorityTaskWoken); +}; + +#endif + +} // namespace concurrency \ No newline at end of file diff --git a/src/concurrency/InterruptableDelay.cpp b/src/concurrency/InterruptableDelay.cpp new file mode 100644 index 0000000..8bc8543 --- /dev/null +++ b/src/concurrency/InterruptableDelay.cpp @@ -0,0 +1,35 @@ +#include "concurrency/InterruptableDelay.h" +#include "configuration.h" + +namespace concurrency +{ + +InterruptableDelay::InterruptableDelay() {} + +InterruptableDelay::~InterruptableDelay() {} + +/** + * Returns false if we were interrupted + */ +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); + + // LOG_DEBUG("interrupt=%d", r); + return !r; +} + +void InterruptableDelay::interrupt() +{ + semaphore.give(); +} + +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 new file mode 100644 index 0000000..41bc40a --- /dev/null +++ b/src/concurrency/InterruptableDelay.h @@ -0,0 +1,41 @@ +#pragma once + +#include "../freertosinc.h" + +#ifdef HAS_FREE_RTOS +#include "concurrency/BinarySemaphoreFreeRTOS.h" +#define BinarySemaphore BinarySemaphoreFreeRTOS +#else +#include "concurrency/BinarySemaphorePosix.h" +#define BinarySemaphore BinarySemaphorePosix +#endif + +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. + * + * This is implemented for FreeRTOS but should be easy to port to other operating systems. + */ +class InterruptableDelay +{ + BinarySemaphore semaphore; + + public: + InterruptableDelay(); + ~InterruptableDelay(); + + /** + * Returns false if we were interrupted + */ + bool delay(uint32_t msec); + + void interrupt(); + + 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 new file mode 100644 index 0000000..1150135 --- /dev/null +++ b/src/concurrency/Lock.cpp @@ -0,0 +1,32 @@ +#include "Lock.h" +#include "configuration.h" +#include + +namespace concurrency +{ + +#ifdef HAS_FREE_RTOS +Lock::Lock() : handle(xSemaphoreCreateBinary()) +{ + assert(handle); + assert(xSemaphoreGive(handle)); +} + +void Lock::lock() +{ + assert(xSemaphoreTake(handle, portMAX_DELAY)); +} + +void Lock::unlock() +{ + assert(xSemaphoreGive(handle)); +} +#else +Lock::Lock() {} + +void Lock::lock() {} + +void Lock::unlock() {} +#endif + +} // namespace concurrency diff --git a/src/concurrency/Lock.h b/src/concurrency/Lock.h new file mode 100644 index 0000000..1b9ea20 --- /dev/null +++ b/src/concurrency/Lock.h @@ -0,0 +1,35 @@ +#pragma once + +#include "../freertosinc.h" + +namespace concurrency +{ + +/** + * @brief Simple wrapper around FreeRTOS API for implementing a mutex lock + */ +class Lock +{ + public: + Lock(); + + Lock(const Lock &) = delete; + Lock &operator=(const Lock &) = delete; + + /// Locks the lock. + // + // Must not be called from an ISR. + void lock(); + + // Unlocks the lock. + // + // Must not be called from an ISR. + void unlock(); + + private: +#ifdef HAS_FREE_RTOS + SemaphoreHandle_t handle; +#endif +}; + +} // namespace concurrency diff --git a/src/concurrency/LockGuard.cpp b/src/concurrency/LockGuard.cpp new file mode 100644 index 0000000..d855266 --- /dev/null +++ b/src/concurrency/LockGuard.cpp @@ -0,0 +1,17 @@ +#include "LockGuard.h" +#include "configuration.h" + +namespace concurrency +{ + +LockGuard::LockGuard(Lock *lock) : lock(lock) +{ + lock->lock(); +} + +LockGuard::~LockGuard() +{ + lock->unlock(); +} + +} // namespace concurrency diff --git a/src/concurrency/LockGuard.h b/src/concurrency/LockGuard.h new file mode 100644 index 0000000..647e796 --- /dev/null +++ b/src/concurrency/LockGuard.h @@ -0,0 +1,24 @@ +#pragma once + +#include "Lock.h" + +namespace concurrency +{ + +/** + * @brief RAII lock guard + */ +class LockGuard +{ + public: + explicit LockGuard(Lock *lock); + ~LockGuard(); + + LockGuard(const LockGuard &) = delete; + LockGuard &operator=(const LockGuard &) = delete; + + private: + Lock *lock; +}; + +} // namespace concurrency diff --git a/src/concurrency/NotifiedWorkerThread.cpp b/src/concurrency/NotifiedWorkerThread.cpp new file mode 100644 index 0000000..d84ff09 --- /dev/null +++ b/src/concurrency/NotifiedWorkerThread.cpp @@ -0,0 +1,94 @@ +#include "NotifiedWorkerThread.h" +#include "configuration.h" +#include "main.h" + +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); + + if (r) + mainDelay.interrupt(); + + 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; + + notification = v; + if (debugNotification) { + LOG_DEBUG("setting notification %d", v); + } + return true; + } else { + if (debugNotification) { + LOG_DEBUG("dropping notification %d", v); + } + return false; + } +} + +/** + * Notify from an ISR + * + * 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); + + 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); + + 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("delaying notification %u", delay); + } + } + + return didIt; +} + +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(); + + return RUN_SAME; +} + +} // namespace concurrency \ No newline at end of file diff --git a/src/concurrency/NotifiedWorkerThread.h b/src/concurrency/NotifiedWorkerThread.h new file mode 100644 index 0000000..7a150b0 --- /dev/null +++ b/src/concurrency/NotifiedWorkerThread.h @@ -0,0 +1,56 @@ +#pragma once + +#include "OSThread.h" + +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; + + public: + NotifiedWorkerThread(const char *name) : OSThread(name) {} + + /** + * 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); + + /** + * 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; + + /// 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(); + + 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 new file mode 100644 index 0000000..d9bb901 --- /dev/null +++ b/src/concurrency/OSThread.cpp @@ -0,0 +1,142 @@ +#include "OSThread.h" +#include "configuration.h" +#include "memGet.h" +#include + +namespace concurrency +{ + +/// Show debugging info for disabled threads +bool OSThread::showDisabled; + +/// Show debugging info for threads when we run them +bool OSThread::showRun = false; + +/// Show debugging info for threads we decide not to run; +bool OSThread::showWaiting = false; + +const OSThread *OSThread::currentThread; + +ThreadController mainController, timerController; +InterruptableDelay mainDelay; + +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(); + + ThreadName = _name; + + if (controller) { + bool added = controller->add(this); + assert(added); + } +} + +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; + + // 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); + + 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 (showDisabled && !enabled) { + LOG_DEBUG("Thread %s: disabled", ThreadName.c_str()); + } + + return r; +} + +void OSThread::run() +{ +#ifdef DEBUG_HEAP + auto heap = memGet.getFreeHeap(); +#endif + currentThread = this; + auto newDelay = runOnce(); +#ifdef DEBUG_HEAP + auto newHeap = memGet.getFreeHeap(); + if (newHeap < heap) + LOG_DEBUG("------ Thread %s leaked heap %d -> %d (%d) ------", ThreadName.c_str(), heap, newHeap, newHeap - heap); + if (heap < newHeap) + LOG_DEBUG("++++++ Thread %s freed heap %d -> %d (%d) ++++++", ThreadName.c_str(), heap, newHeap, newHeap - heap); +#endif + + runned(); + + if (newDelay >= 0) + setInterval(newDelay); + + currentThread = NULL; +} + +int32_t OSThread::disable() +{ + enabled = false; + setInterval(INT32_MAX); + + return INT32_MAX; +} + +/** + * 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. + */ +bool hasBeenSetup; + +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); +} + +} // namespace concurrency diff --git a/src/concurrency/OSThread.h b/src/concurrency/OSThread.h new file mode 100644 index 0000000..0fbfe2e --- /dev/null +++ b/src/concurrency/OSThread.h @@ -0,0 +1,91 @@ +#pragma once + +#include +#include + +#include "Thread.h" +#include "ThreadController.h" +#include "concurrency/InterruptableDelay.h" + +namespace concurrency +{ + +extern ThreadController mainController, timerController; +extern InterruptableDelay mainDelay; + +#define RUN_SAME -1 + +/** + * @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. + * + * TODO FIXME @geeksville + * + * move more things into OSThreads + * remove lock/lockguard + * + * move typedQueue into concurrency + * remove freertos from typedqueue + */ +class OSThread : public Thread +{ + ThreadController *controller; + + /// 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 we decide not to run; + static bool showWaiting; + + public: + /// For debug printing only (might be null) + static const OSThread *currentThread; + + OSThread(const char *name, uint32_t period = 0, ThreadController *controller = &mainController); + + virtual ~OSThread(); + + virtual bool shouldRun(unsigned long time); + + static void setup(); + + 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); + + 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(); +}; + +/** + * 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. + */ +extern bool hasBeenSetup; + +void assertIsSetup(); + +} // namespace concurrency \ No newline at end of file diff --git a/src/concurrency/Periodic.h b/src/concurrency/Periodic.h new file mode 100644 index 0000000..db07145 --- /dev/null +++ b/src/concurrency/Periodic.h @@ -0,0 +1,24 @@ +#pragma once + +#include "concurrency/OSThread.h" + +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)(); + + 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(); } +}; + +} // namespace concurrency diff --git a/src/configuration.h b/src/configuration.h new file mode 100644 index 0000000..cb23264 --- /dev/null +++ b/src/configuration.h @@ -0,0 +1,360 @@ +/* + +TTGO T-BEAM Tracker for The Things Network + +Copyright (C) 2018 by Xose Pérez + +This code requires LMIC library by Matthijs Kooijman +https://github.com/matthijskooijman/arduino-lmic + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#pragma once + +#include + +#ifdef RV3028_RTC +#include "Melopero_RV3028.h" +#endif +#ifdef PCF8563_RTC +#include "pcf8563.h" +#endif + +// ----------------------------------------------------------------------------- +// Version +// ----------------------------------------------------------------------------- + +// If app version is not specified we assume we are not being invoked by the build script +#ifndef APP_VERSION +#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. +#ifndef HW_VERSION +#define HW_VERSION "1.0" +#endif + +// ----------------------------------------------------------------------------- +// Configuration +// ----------------------------------------------------------------------------- + +/// Convert a preprocessor name into a quoted string +#define xstr(s) ystr(s) +#define ystr(s) #s + +/// Convert a preprocessor name into a quoted string and if that string is empty use "unset" +#define optstr(s) (xstr(s)[0] ? xstr(s) : "unset") + +// Nop definition for these attributes that are specific to ESP32 +#ifndef EXT_RAM_ATTR +#define EXT_RAM_ATTR +#endif +#ifndef IRAM_ATTR +#define IRAM_ATTR +#endif +#ifndef RTC_DATA_ATTR +#define RTC_DATA_ATTR +#endif +#ifndef EXT_RAM_BSS_ATTR +#define EXT_RAM_BSS_ATTR EXT_RAM_ATTR +#endif + +// ----------------------------------------------------------------------------- +// Regulatory overrides +// ----------------------------------------------------------------------------- + +// Override user saved region, for producing region-locked builds +// #define REGULATORY_LORA_REGIONCODE meshtastic_Config_LoRaConfig_RegionCode_SG_923 + +// Total system gain in dBm to subtract from Tx power to remain within regulatory ERP limit for non-licensed operators +// This value should be set in variant.h and is PA gain + antenna gain (if system ships with an antenna) +#ifndef REGULATORY_GAIN_LORA +#define REGULATORY_GAIN_LORA 0 +#endif + +// ----------------------------------------------------------------------------- +// Feature toggles +// ----------------------------------------------------------------------------- + +// Disable use of the NTP library and related features +// #define DISABLE_NTP + +// Disable the welcome screen and allow +// #define DISABLE_WELCOME_UNSET + +// ----------------------------------------------------------------------------- +// OLED & Input +// ----------------------------------------------------------------------------- + +#define SSD1306_ADDRESS 0x3C +#define ST7567_ADDRESS 0x3F + +// The SH1106 controller is almost, but not quite, the same as SSD1306 +// Define this if you know you have that controller or your "SSD1306" misbehaves. +// #define USE_SH1106 + +// Define if screen should be mirrored left to right +// #define SCREEN_MIRROR + +// I2C Keyboards (M5Stack, RAK14004, T-Deck) +#define CARDKB_ADDR 0x5F +#define TDECK_KB_ADDR 0x55 +#define BBQ10_KB_ADDR 0x1F +#define MPR121_KB_ADDR 0x5A + +// ----------------------------------------------------------------------------- +// SENSOR +// ----------------------------------------------------------------------------- +#define BME_ADDR 0x76 +#define BME_ADDR_ALTERNATE 0x77 +#define MCP9808_ADDR 0x18 +#define INA_ADDR 0x40 +#define INA_ADDR_ALTERNATE 0x41 +#define INA_ADDR_WAVESHARE_UPS 0x43 +#define INA3221_ADDR 0x42 +#define MAX1704X_ADDR 0x36 +#define QMC6310_ADDR 0x1C +#define QMI8658_ADDR 0x6B +#define QMC5883L_ADDR 0x0D +#define HMC5883L_ADDR 0x1E +#define SHTC3_ADDR 0x70 +#define LPS22HB_ADDR 0x5C +#define LPS22HB_ADDR_ALT 0x5D +#define SHT31_4x_ADDR 0x44 +#define PMSA0031_ADDR 0x12 +#define QMA6100P_ADDR 0x12 +#define AHT10_ADDR 0x38 +#define RCWL9620_ADDR 0x57 +#define VEML7700_ADDR 0x10 +#define TSL25911_ADDR 0x29 +#define OPT3001_ADDR 0x45 +#define OPT3001_ADDR_ALT 0x44 +#define MLX90632_ADDR 0x3A +#define DFROBOT_LARK_ADDR 0x42 +#define NAU7802_ADDR 0x2A +#define MAX30102_ADDR 0x57 +#define MLX90614_ADDR_DEF 0x5A + +// ----------------------------------------------------------------------------- +// ACCELEROMETER +// ----------------------------------------------------------------------------- +#define MPU6050_ADDR 0x68 +#define STK8BXX_ADR 0x18 +#define LIS3DH_ADR 0x18 +#define BMA423_ADDR 0x19 +#define LSM6DS3_ADDR 0x6A +#define BMX160_ADDR 0x69 +#define ICM20948_ADDR 0x69 +#define ICM20948_ADDR_ALT 0x68 + +// ----------------------------------------------------------------------------- +// LED +// ----------------------------------------------------------------------------- +#define NCP5623_ADDR 0x38 + +// ----------------------------------------------------------------------------- +// Security +// ----------------------------------------------------------------------------- +#define ATECC608B_ADDR 0x35 + +// ----------------------------------------------------------------------------- +// IO Expander +// ----------------------------------------------------------------------------- +#define TCA9535_ADDR 0x20 +#define TCA9555_ADDR 0x26 + +// ----------------------------------------------------------------------------- +// GPS +// ----------------------------------------------------------------------------- +#ifndef GPS_THREAD_INTERVAL +#define GPS_THREAD_INTERVAL 200 +#endif + +// ----------------------------------------------------------------------------- +// Touchscreen +// ----------------------------------------------------------------------------- +#define FT6336U_ADDR 0x48 + +// ----------------------------------------------------------------------------- +// BIAS-T Generator +// ----------------------------------------------------------------------------- +#define TPS65233_ADDR 0x60 + +// convert 24-bit color to 16-bit (56K) +#define COLOR565(r, g, b) (((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3)) + +/* Step #1: offer chance for variant-specific defines */ +#include "variant.h" + +#if defined(VEXT_ENABLE) && !defined(VEXT_ON_VALUE) +// Older variant.h files might not be defining this value, so stay with the old default +#define VEXT_ON_VALUE LOW +#endif + +#ifndef GPS_BAUDRATE +#define GPS_BAUDRATE 9600 +#endif + +/* Step #2: follow with defines common to the architecture; + also enable HAS_ option not specifically disabled by variant.h */ +#include "architecture.h" + +#ifndef DEFAULT_REBOOT_SECONDS +#define DEFAULT_REBOOT_SECONDS 7 +#endif + +#ifndef DEFAULT_SHUTDOWN_SECONDS +#define DEFAULT_SHUTDOWN_SECONDS 2 +#endif + +#ifndef MINIMUM_SAFE_FREE_HEAP +#define MINIMUM_SAFE_FREE_HEAP 1500 +#endif + +#ifndef WIRE_INTERFACES_COUNT +// Officially an NRF52 macro +// Repurposed cross-platform to identify devices using Wire1 +#if defined(I2C_SDA1) || defined(PIN_WIRE1_SDA) +#define WIRE_INTERFACES_COUNT 2 +#elif HAS_WIRE +#define WIRE_INTERFACES_COUNT 1 +#endif +#endif + +/* Step #3: mop up with disabled values for HAS_ options not handled by the above two */ + +#ifndef HAS_WIFI +#define HAS_WIFI 0 +#endif +#ifndef HAS_ETHERNET +#define HAS_ETHERNET 0 +#endif +#ifndef HAS_SCREEN +#define HAS_SCREEN 0 +#endif +#ifndef HAS_WIRE +#define HAS_WIRE 0 +#endif +#ifndef HAS_GPS +#define HAS_GPS 0 +#endif +#ifndef HAS_BUTTON +#define HAS_BUTTON 0 +#endif +#ifndef HAS_TRACKBALL +#define HAS_TRACKBALL 0 +#endif +#ifndef HAS_TOUCHSCREEN +#define HAS_TOUCHSCREEN 0 +#endif +#ifndef HAS_TELEMETRY +#define HAS_TELEMETRY 0 +#endif +#ifndef HAS_SENSOR +#define HAS_SENSOR 0 +#endif +#ifndef HAS_RADIO +#define HAS_RADIO 0 +#endif +#ifndef HAS_RTC +#define HAS_RTC 0 +#endif +#ifndef HAS_CPU_SHUTDOWN +#define HAS_CPU_SHUTDOWN 0 +#endif +#ifndef HAS_BLUETOOTH +#define HAS_BLUETOOTH 0 +#endif + +#ifndef HW_VENDOR +#error HW_VENDOR must be defined +#endif + +// ----------------------------------------------------------------------------- +// Global switches to turn off features for a minimized build +// ----------------------------------------------------------------------------- + +// #define MESHTASTIC_MINIMIZE_BUILD 1 +#ifdef MESHTASTIC_MINIMIZE_BUILD +#define MESHTASTIC_EXCLUDE_MODULES 1 +#define MESHTASTIC_EXCLUDE_WIFI 1 +#define MESHTASTIC_EXCLUDE_BLUETOOTH 1 +#define MESHTASTIC_EXCLUDE_GPS 1 +#define MESHTASTIC_EXCLUDE_SCREEN 1 +#define MESHTASTIC_EXCLUDE_MQTT 1 +#define MESHTASTIC_EXCLUDE_POWERMON 1 +#define MESHTASTIC_EXCLUDE_I2C 1 +#define MESHTASTIC_EXCLUDE_PKI 1 +#define MESHTASTIC_EXCLUDE_POWER_FSM 1 +#define MESHTASTIC_EXCLUDE_TZ 1 +#endif + +// Turn off all optional modules +#ifdef MESHTASTIC_EXCLUDE_MODULES +#define MESHTASTIC_EXCLUDE_AUDIO 1 +#define MESHTASTIC_EXCLUDE_DETECTIONSENSOR 1 +#define MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR 1 +#define MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION 1 +#define MESHTASTIC_EXCLUDE_PAXCOUNTER 1 +#define MESHTASTIC_EXCLUDE_POWER_TELEMETRY 1 +#define MESHTASTIC_EXCLUDE_RANGETEST 1 +#define MESHTASTIC_EXCLUDE_REMOTEHARDWARE 1 +#define MESHTASTIC_EXCLUDE_STOREFORWARD 1 +#define MESHTASTIC_EXCLUDE_TEXTMESSAGE 1 +#define MESHTASTIC_EXCLUDE_ATAK 1 +#define MESHTASTIC_EXCLUDE_CANNEDMESSAGES 1 +#define MESHTASTIC_EXCLUDE_NEIGHBORINFO 1 +#define MESHTASTIC_EXCLUDE_TRACEROUTE 1 +#define MESHTASTIC_EXCLUDE_WAYPOINT 1 +#define MESHTASTIC_EXCLUDE_INPUTBROKER 1 +#define MESHTASTIC_EXCLUDE_SERIAL 1 +#define MESHTASTIC_EXCLUDE_POWERSTRESS 1 +#define MESHTASTIC_EXCLUDE_ADMIN 1 +#endif + +// // Turn off wifi even if HW supports wifi (webserver relies on wifi and is also disabled) +#ifdef MESHTASTIC_EXCLUDE_WIFI +#define MESHTASTIC_EXCLUDE_WEBSERVER 1 +#undef HAS_WIFI +#define HAS_WIFI 0 +#endif + +// Allow code that needs internet to just check HAS_NETWORKING rather than HAS_WIFI || HAS_ETHERNET +#define HAS_NETWORKING (HAS_WIFI || HAS_ETHERNET) + +// // Turn off Bluetooth +#ifdef MESHTASTIC_EXCLUDE_BLUETOOTH +#undef HAS_BLUETOOTH +#define HAS_BLUETOOTH 0 +#endif + +// // Turn off GPS +#ifdef MESHTASTIC_EXCLUDE_GPS +#undef HAS_GPS +#define HAS_GPS 0 +#undef MESHTASTIC_EXCLUDE_RANGETEST +#define MESHTASTIC_EXCLUDE_RANGETEST 1 +#endif + +// Turn off Screen +#ifdef MESHTASTIC_EXCLUDE_SCREEN +#undef HAS_SCREEN +#define HAS_SCREEN 0 +#endif + +#include "DebugConfiguration.h" +#include "RF95Configuration.h" diff --git a/src/detect/LoRaRadioType.h b/src/detect/LoRaRadioType.h new file mode 100644 index 0000000..a059a36 --- /dev/null +++ b/src/detect/LoRaRadioType.h @@ -0,0 +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 +}; + +extern LoRaRadioType radioType; \ No newline at end of file diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp new file mode 100644 index 0000000..4caa0f7 --- /dev/null +++ b/src/detect/ScanI2C.cpp @@ -0,0 +1,77 @@ +#include "ScanI2C.h" + +const ScanI2C::DeviceAddress ScanI2C::ADDRESS_NONE = ScanI2C::DeviceAddress(); +const ScanI2C::FoundDevice ScanI2C::DEVICE_NONE = ScanI2C::FoundDevice(ScanI2C::DeviceType::NONE, ADDRESS_NONE); + +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; +} + +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}; + return firstOfOrNONE(2, types); +} + +ScanI2C::FoundDevice ScanI2C::firstKeyboard() const +{ + ScanI2C::DeviceType types[] = {CARDKB, TDECKKB, BBQ10KB, RAK14004, MPR121KB}; + return firstOfOrNONE(5, types); +} + +ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const +{ + ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P}; + return firstOfOrNONE(8, 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)); +} + +ScanI2C::FoundDevice::FoundDevice(ScanI2C::DeviceType type, ScanI2C::DeviceAddress address) : type(type), address(address) {} \ No newline at end of file diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h new file mode 100644 index 0000000..8591b84 --- /dev/null +++ b/src/detect/ScanI2C.h @@ -0,0 +1,130 @@ +#pragma once + +#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, + ATECC608B, + RTC_RV3028, + RTC_PCF8563, + CARDKB, + TDECKKB, + BBQ10KB, + RAK14004, + PMU_AXP192_AXP2101, + 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, + TSL2591, + OPT3001, + MLX90632, + MLX90614, + AHT10, + BMX160, + DFROBOT_LARK, + NAU7802, + FT6336U, + STK8BAXX, + ICM20948, + MAX30102, + TPS65233, + MPR121KB + } DeviceType; + + // 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; + + explicit DeviceAddress(I2CPort port, uint8_t address); + DeviceAddress(); + + bool operator<(const DeviceAddress &other) const; + } DeviceAddress; + + static const DeviceAddress ADDRESS_NONE; + + typedef uint8_t RegisterAddress; + + typedef struct FoundDevice { + DeviceType type; + DeviceAddress address; + + explicit FoundDevice(DeviceType = DeviceType::NONE, DeviceAddress = ADDRESS_NONE); + } FoundDevice; + + static const FoundDevice DEVICE_NONE; + + public: + ScanI2C(); + + 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(); + + FoundDevice firstScreen() const; + + FoundDevice firstRTC() const; + + FoundDevice firstKeyboard() const; + + FoundDevice firstAccelerometer() const; + + virtual FoundDevice find(DeviceType) const; + + virtual bool exists(DeviceType) const; + + virtual size_t countDevices() const; + + protected: + virtual FoundDevice firstOfOrNONE(size_t, DeviceType[]) const; + + private: + bool shouldSuppressScreen = false; +}; \ No newline at end of file diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp new file mode 100644 index 0000000..d39c989 --- /dev/null +++ b/src/detect/ScanI2CTwoWire.cpp @@ -0,0 +1,493 @@ +#include "ScanI2CTwoWire.h" + +#if !MESHTASTIC_EXCLUDE_I2C + +#include "concurrency/LockGuard.h" +#if defined(ARCH_PORTDUINO) +#include "linux/LinuxHardwareI2C.h" +#endif +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) +#include "main.h" // atecc +#include "meshUtils.h" // vformat +#endif + +// AXP192 and AXP2101 have the same device address, we just need to identify it in Power.cpp +#ifndef XPOWERS_AXP192_AXP2101_ADDRESS +#define XPOWERS_AXP192_AXP2101_ADDRESS 0x34 +#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; +} + +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; +} + +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); + + for (size_t k = 0; k < count; k++) { + ScanI2C::DeviceType current = types[k]; + + if (exists(current)) { + return ScanI2C::FoundDevice(current, deviceAddresses.at(current)); + } + } + + return DEVICE_NONE; +} + +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) { + LOG_INFO("sh1106 display found"); + o_probe = SCREEN_SH1106; // SH1106 + } else if (r == 0x03 || r == 0x04 || r == 0x06 || r == 0x07) { + LOG_INFO("ssd1306 display found"); + 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; +} +void ScanI2CTwoWire::printATECCInfo() const +{ +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) + atecc.readConfigZone(false); + + std::string atecc_numbers = "ATECC608B Serial Number: "; + for (int i = 0; i < 9; i++) { + atecc_numbers += vformat("%02x", atecc.serialNumber[i]); + } + + atecc_numbers += ", Rev Number: "; + for (int i = 0; i < 4; i++) { + atecc_numbers += vformat("%02x", atecc.revisionNumber[i]); + } + LOG_DEBUG(atecc_numbers.c_str()); + + LOG_DEBUG("ATECC608B Config %s, Data %s, Slot 0 %s", atecc.configLockStatus ? "Locked" : "Unlocked", + atecc.dataOTPLockStatus ? "Locked" : "Unlocked", atecc.slot0LockStatus ? "Locked" : "Unlocked"); + + std::string atecc_publickey = ""; + if (atecc.configLockStatus && atecc.dataOTPLockStatus && atecc.slot0LockStatus) { + if (atecc.generatePublicKey() == false) { + atecc_publickey += "ATECC608B Error generating public key"; + } else { + atecc_publickey += "ATECC608B Public Key: "; + for (int i = 0; i < 64; i++) { + atecc_publickey += vformat("%02x", atecc.publicKey64Bytes[i]); + } + } + LOG_DEBUG(atecc_publickey.c_str()); + } +#endif +} + +uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation ®isterLocation, + ScanI2CTwoWire::ResponseWidth responseWidth) const +{ + uint16_t value = 0x00; + TwoWire *i2cBus = fetchI2CBus(registerLocation.i2cAddress); + + i2cBus->beginTransmission(registerLocation.i2cAddress.address); + i2cBus->write(registerLocation.registerAddress); + i2cBus->endTransmission(); + delay(20); + i2cBus->requestFrom(registerLocation.i2cAddress.address, responseWidth); + LOG_DEBUG("Wire.available() = %d", i2cBus->available()); + if (i2cBus->available() == 2) { + // Read MSB, then LSB + value = (uint16_t)i2cBus->read() << 8; + value |= i2cBus->read(); + } else if (i2cBus->available()) { + value = i2cBus->read(); + } + return value; +} + +#define SCAN_SIMPLE_CASE(ADDR, T, ...) \ + case ADDR: \ + LOG_INFO(__VA_ARGS__); \ + type = T; \ + break; + +void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) +{ + concurrency::LockGuard guard((concurrency::Lock *)&lock); + + LOG_DEBUG("Scanning 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, addr.address)) + continue; + LOG_DEBUG("Scanning address 0x%x", addr.address); + } + i2cBus->beginTransmission(addr.address); +#ifdef ARCH_PORTDUINO + if (i2cBus->read() != -1) + err = 0; + else + err = 2; +#else + err = i2cBus->endTransmission(); +#endif + type = NONE; + if (err == 0) { + LOG_DEBUG("I2C device found at address 0x%x", addr.address); + + switch (addr.address) { + case SSD1306_ADDRESS: + type = probeOLED(addr); + break; + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) + case ATECC608B_ADDR: +#ifdef RP2040_SLOW_CLOCK + if (atecc.begin(addr.address, Wire, Serial2) == true) +#else + if (atecc.begin(addr.address) == true) +#endif + + { + LOG_INFO("ATECC608B initialized"); + } else { + LOG_WARN("ATECC608B initialization failed"); + } + printATECCInfo(); + break; +#endif + +#ifdef RV3028_RTC + case RV3028_RTC: + // foundDevices[addr] = RTC_RV3028; + type = RTC_RV3028; + LOG_INFO("RV3028 RTC found"); + rtc.initI2C(*i2cBus); + rtc.writeToRegister(0x35, 0x07); // no Clkout + rtc.writeToRegister(0x37, 0xB4); + break; +#endif + +#ifdef PCF8563_RTC + SCAN_SIMPLE_CASE(PCF8563_RTC, RTC_PCF8563, "PCF8563 RTC found") +#endif + + case CARDKB_ADDR: + // Do we have the RAK14006 instead? + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x04), 1); + if (registerValue == 0x02) { + // KEYPAD_VERSION + LOG_INFO("RAK14004 found"); + type = RAK14004; + } else { + LOG_INFO("m5 cardKB found"); + type = CARDKB; + } + break; + + SCAN_SIMPLE_CASE(TDECK_KB_ADDR, TDECKKB, "T-Deck keyboard found"); + SCAN_SIMPLE_CASE(BBQ10_KB_ADDR, BBQ10KB, "BB Q10 keyboard found"); + + SCAN_SIMPLE_CASE(ST7567_ADDRESS, SCREEN_ST7567, "st7567 display found"); +#ifdef HAS_NCP5623 + SCAN_SIMPLE_CASE(NCP5623_ADDR, NCP5623, "NCP5623 RGB LED found"); +#endif +#ifdef HAS_PMU + SCAN_SIMPLE_CASE(XPOWERS_AXP192_AXP2101_ADDRESS, PMU_AXP192_AXP2101, "axp192/axp2101 PMU found") +#endif + case BME_ADDR: + case BME_ADDR_ALTERNATE: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xD0), 1); // GET_ID + switch (registerValue) { + case 0x61: + LOG_INFO("BME-680 sensor found at address 0x%x", (uint8_t)addr.address); + type = BME_680; + break; + case 0x60: + LOG_INFO("BME-280 sensor found at address 0x%x", (uint8_t)addr.address); + type = BME_280; + break; + case 0x55: + LOG_INFO("BMP-085 or BMP-180 sensor found at address 0x%x", (uint8_t)addr.address); + type = BMP_085; + break; + default: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // GET_ID + switch (registerValue) { + case 0x50: // BMP-388 should be 0x50 + LOG_INFO("BMP-388 sensor found at address 0x%x", (uint8_t)addr.address); + type = BMP_3XX; + break; + case 0x58: // BMP-280 should be 0x58 + default: + LOG_INFO("BMP-280 sensor found at address 0x%x", (uint8_t)addr.address); + type = BMP_280; + break; + } + break; + } + break; +#ifndef HAS_NCP5623 + case AHT10_ADDR: + LOG_INFO("AHT10 sensor found at address 0x%x", (uint8_t)addr.address); + type = AHT10; + break; +#endif + 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) { + LOG_INFO("INA260 sensor found at address 0x%x", (uint8_t)addr.address); + type = INA260; + } else { // Assume INA219 if INA260 ID is not found + LOG_INFO("INA219 sensor found at address 0x%x", (uint8_t)addr.address); + type = INA219; + } + break; + case INA3221_ADDR: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2); + LOG_DEBUG("Register MFG_UID: 0x%x", registerValue); + if (registerValue == 0x5449) { + LOG_INFO("INA3221 sensor found at address 0x%x", (uint8_t)addr.address); + type = INA3221; + } else { + LOG_INFO("DFRobot Lark weather station found at address 0x%x", (uint8_t)addr.address); + type = DFROBOT_LARK; + } + break; + 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. + { + // Check register 0x00 for 0x8700 response to ID STK8BA53 chip. + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 2); + if (registerValue == 0x8700) { + type = STK8BAXX; + LOG_INFO("STK8BAXX accelerometer found"); + break; + } + + // Check register 0x07 for 0x0400 response to ID MCP9808 chip. + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x07), 2); + if (registerValue == 0x0400) { + type = MCP9808; + LOG_INFO("MCP9808 sensor found"); + 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; + LOG_INFO("LIS3DH accelerometer found"); + } + break; + } + case SHT31_4x_ADDR: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2); + if (registerValue == 0x11a2 || registerValue == 0x11da || registerValue == 0xe9c) { + type = SHT4X; + LOG_INFO("SHT4X sensor found"); + } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) { + type = OPT3001; + LOG_INFO("OPT3001 light sensor found"); + } else { + type = SHT31; + LOG_INFO("SHT31 sensor found"); + } + + break; + + SCAN_SIMPLE_CASE(SHTC3_ADDR, SHTC3, "SHTC3 sensor found") + case RCWL9620_ADDR: + // get MAX30102 PARTID + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 1); + if (registerValue == 0x15) { + type = MAX30102; + LOG_INFO("MAX30102 Health sensor found"); + break; + } else { + type = RCWL9620; + LOG_INFO("RCWL9620 sensor found"); + } + break; + + case LPS22HB_ADDR_ALT: + SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB sensor found") + + SCAN_SIMPLE_CASE(QMC6310_ADDR, QMC6310, "QMC6310 Highrate 3-Axis magnetic sensor found") + + case QMI8658_ADDR: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0A), 1); // get ID + if (registerValue == 0xC0) { + type = BQ24295; + LOG_INFO("BQ24295 PMU found"); + break; + } + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 1); // get ID + if (registerValue == 0x6A) { + type = LSM6DS3; + LOG_INFO("LSM6DS3 accelerometer found at address 0x%x", (uint8_t)addr.address); + } else { + type = QMI8658; + LOG_INFO("QMI8658 Highrate 6-Axis inertial measurement sensor found"); + } + break; + + SCAN_SIMPLE_CASE(QMC5883L_ADDR, QMC5883L, "QMC5883L Highrate 3-Axis magnetic sensor found") + SCAN_SIMPLE_CASE(HMC5883L_ADDR, HMC5883L, "HMC5883L 3-Axis digital compass found") +#ifdef HAS_QMA6100P + SCAN_SIMPLE_CASE(QMA6100P_ADDR, QMA6100P, "QMA6100P accelerometer found") +#else + SCAN_SIMPLE_CASE(PMSA0031_ADDR, PMSA0031, "PMSA0031 air quality sensor found") +#endif + SCAN_SIMPLE_CASE(BMA423_ADDR, BMA423, "BMA423 accelerometer found"); + SCAN_SIMPLE_CASE(LSM6DS3_ADDR, LSM6DS3, "LSM6DS3 accelerometer found at address 0x%x", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(TCA9535_ADDR, TCA9535, "TCA9535 I2C expander found"); + SCAN_SIMPLE_CASE(TCA9555_ADDR, TCA9555, "TCA9555 I2C expander found"); + SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700 light sensor found"); + SCAN_SIMPLE_CASE(TSL25911_ADDR, TSL2591, "TSL2591 light sensor found"); + SCAN_SIMPLE_CASE(OPT3001_ADDR, OPT3001, "OPT3001 light sensor found"); + SCAN_SIMPLE_CASE(MLX90632_ADDR, MLX90632, "MLX90632 IR temp sensor found"); + SCAN_SIMPLE_CASE(NAU7802_ADDR, NAU7802, "NAU7802 based scale found"); + SCAN_SIMPLE_CASE(FT6336U_ADDR, FT6336U, "FT6336U touchscreen found"); + SCAN_SIMPLE_CASE(MAX1704X_ADDR, MAX17048, "MAX17048 lipo fuel gauge found"); +#ifdef HAS_TPS65233 + SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233 BIAS-T found"); +#endif + + case MLX90614_ADDR_DEF: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0e), 1); + if (registerValue == 0x5a) { + type = MLX90614; + LOG_INFO("MLX90614 IR temp sensor found"); + } else { + type = MPR121KB; + LOG_INFO("MPR121KB keyboard found"); + } + break; + + case ICM20948_ADDR: // same as BMX160_ADDR + case ICM20948_ADDR_ALT: // same as MPU6050_ADDR + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); + if (registerValue == 0xEA) { + type = ICM20948; + LOG_INFO("ICM20948 9-dof motion processor found"); + break; + } else if (addr.address == BMX160_ADDR) { + type = BMX160; + LOG_INFO("BMX160 accelerometer found"); + break; + } else { + type = MPU6050; + LOG_INFO("MPU6050 accelerometer found"); + break; + } + break; + + default: + LOG_INFO("Device found at address 0x%x was not able to be enumerated", addr.address); + } + } else if (err == 4) { + LOG_ERROR("Unknown error at address 0x%x", 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); +} + +TwoWire *ScanI2CTwoWire::fetchI2CBus(ScanI2C::DeviceAddress address) const +{ + if (address.port == ScanI2C::I2CPort::WIRE) { + return &Wire; + } else { +#if WIRE_INTERFACES_COUNT == 2 + return &Wire1; +#else + return &Wire; +#endif + } +} + +size_t ScanI2CTwoWire::countDevices() const +{ + return foundDevices.size(); +} +#endif \ No newline at end of file diff --git a/src/detect/ScanI2CTwoWire.h b/src/detect/ScanI2CTwoWire.h new file mode 100644 index 0000000..c8dd964 --- /dev/null +++ b/src/detect/ScanI2CTwoWire.h @@ -0,0 +1,62 @@ +#pragma once + +#include "configuration.h" +#if !MESHTASTIC_EXCLUDE_I2C + +#include +#include +#include +#include + +#include + +#include "ScanI2C.h" + +#include "../concurrency/Lock.h" + +class ScanI2CTwoWire : public ScanI2C +{ + public: + void scanPort(ScanI2C::I2CPort) override; + + void scanPort(ScanI2C::I2CPort, uint8_t *, uint8_t) override; + + ScanI2C::FoundDevice find(ScanI2C::DeviceType) const override; + + TwoWire *fetchI2CBus(ScanI2C::DeviceAddress) const; + + bool exists(ScanI2C::DeviceType) const override; + + size_t countDevices() const override; + + protected: + FoundDevice firstOfOrNONE(size_t, DeviceType[]) const override; + + private: + typedef struct RegisterLocation { + DeviceAddress i2cAddress; + RegisterAddress registerAddress; + + RegisterLocation(DeviceAddress deviceAddress, RegisterAddress registerAddress) + : i2cAddress(deviceAddress), registerAddress(registerAddress) + { + } + + } RegisterLocation; + + typedef uint8_t ResponseWidth; + + std::map foundDevices; + + // note: prone to overwriting if multiple devices of a type are added at different addresses (rare?) + std::map deviceAddresses; + + concurrency::Lock lock; + + void printATECCInfo() const; + + uint16_t getRegisterValue(const RegisterLocation &, ResponseWidth) const; + + DeviceType probeOLED(ScanI2C::DeviceAddress) const; +}; +#endif \ No newline at end of file diff --git a/src/detect/einkScan.h b/src/detect/einkScan.h new file mode 100644 index 0000000..d20c7b6 --- /dev/null +++ b/src/detect/einkScan.h @@ -0,0 +1,67 @@ +#include "../configuration.h" + +#ifdef RAK_4631 +#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_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; +} + +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/error.h b/src/error.h new file mode 100644 index 0000000..d16ee65 --- /dev/null +++ b/src/error.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +#include "mesh/generated/meshtastic/mesh.pb.h" // For CriticalErrorCode + +/// A macro that include filename and line +#define RECORD_CRITICALERROR(code) recordCriticalError(code, __LINE__, __FILE__) + +/// Record an error that should be reported via analytics +void recordCriticalError(meshtastic_CriticalErrorCode code = meshtastic_CriticalErrorCode_UNSPECIFIED, uint32_t address = 0, + const char *filename = NULL); \ No newline at end of file diff --git a/src/freertosinc.h b/src/freertosinc.h new file mode 100644 index 0000000..e9e6cd5 --- /dev/null +++ b/src/freertosinc.h @@ -0,0 +1,47 @@ +#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 + +#if defined(ARDUINO_ARCH_ESP32) +#define HAS_FREE_RTOS + +#include +#include +#include +#include +#endif + +#if defined(ARDUINO_NRF52_ADAFRUIT) || defined(ARDUINO_ARCH_RP2040) +#define HAS_FREE_RTOS + +#include +#include +#include +#include +#endif + +#ifdef HAS_FREE_RTOS + +// Include real FreeRTOS defs above + +#else + +// Include placeholder fake FreeRTOS defs + +#include + +typedef uint32_t TickType_t; +typedef uint32_t BaseType_t; + +#define portMAX_DELAY UINT32_MAX + +#define tskIDLE_PRIORITY 0 +#define configMAX_PRIORITIES 10 // Highest priority level + +// Don't do anything on non free rtos platforms when done with the ISR +#define portYIELD_FROM_ISR(x) + +enum eNotifyAction { eNoAction, eSetValueWithoutOverwrite, eSetValueWithOverwrite }; + +#endif \ No newline at end of file diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp new file mode 100644 index 0000000..2dda776 --- /dev/null +++ b/src/gps/GPS.cpp @@ -0,0 +1,1713 @@ +#include "configuration.h" +#if !MESHTASTIC_EXCLUDE_GPS +#include "Default.h" +#include "GPS.h" +#include "GpioLogic.h" +#include "NodeDB.h" +#include "PowerMon.h" +#include "RTC.h" +#include "Throttle.h" +#include "buzz.h" +#include "meshUtils.h" + +#include "main.h" // pmu_found +#include "sleep.h" + +#include "GPSUpdateScheduling.h" +#include "cas.h" +#include "ubx.h" + +#ifdef ARCH_PORTDUINO +#include "PortduinoGlue.h" +#include "meshUtils.h" +#include +#include +#endif + +#ifndef GPS_RESET_MODE +#define GPS_RESET_MODE HIGH +#endif + +#if defined(NRF52840_XXAA) || defined(NRF52833_XXAA) || defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) +HardwareSerial *GPS::_serial_gps = &Serial1; +#elif defined(ARCH_RP2040) +SerialUART *GPS::_serial_gps = &Serial1; +#else +HardwareSerial *GPS::_serial_gps = NULL; +#endif + +GPS *gps = nullptr; + +GPSUpdateScheduling scheduling; + +/// Multiple GPS instances might use the same serial port (in sequence), but we can +/// only init that port once. +static bool didSerialInit; + +struct uBloxGnssModelInfo info; +uint8_t uBloxProtocolVersion; +#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 +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 + } +} + +void GPS::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; + } + + // 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 +void GPS::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; + } + + // 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; + + 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); +} + +// 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 | + + // 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; + + 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("Constructed CAS packet: "); + DEBUG_PORT.hexDump(MESHTASTIC_LOG_LEVEL_DEBUG, UBXscratch, payload_size + 10); +#endif + 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; +#ifdef GPS_DEBUG + std::string debugmsg = ""; +#endif + while (millis() < startTimeout) { + if (_serial_gps->available()) { + b = _serial_gps->read(); + +#ifdef GPS_DEBUG + debugmsg += vformat("%c", (b >= 32 && b <= 126) ? b : '.'); +#endif + buffer[bytesRead] = b; + bytesRead++; + if ((bytesRead == 767) || (b == '\r')) { + if (strnstr((char *)buffer, message, bytesRead) != nullptr) { +#ifdef GPS_DEBUG + LOG_DEBUG("Found: %s", message); // Log the found message +#endif + return GNSS_RESPONSE_OK; + } else { + bytesRead = 0; +#ifdef GPS_DEBUG + LOG_DEBUG(debugmsg.c_str()); +#endif + } + } + } + } + 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; + + // 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(); + + // 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 %d millis.", 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 %d millis.", 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; +#ifdef GPS_DEBUG + 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_DEBUG(""); + LOG_INFO("Got ACK for class %02X message %02X in %d millis.", 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()); +#endif + return GNSS_RESPONSE_FRAME_ERRORS; + } + } else { + sCounter = 0; + } +#ifdef GPS_DEBUG + debugmsg += vformat("%02X", b); +#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 + } + } + } +#ifdef GPS_DEBUG + 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 +} + +/** + * @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 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; + + 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 %d millis.", requestedClass, requestedID, + millis() - startTime); +#endif + return needRead; + } + break; + + default: + break; + } + } + } + // LOG_WARN("No response for class %02X message %02X", requestedClass, requestedID); + return 0; +} + +bool GPS::setup() +{ + + if (!didSerialInit) { + int msglen = 0; + if (tx_gpio && gnssModel == GNSS_MODEL_UNKNOWN) { + + // if GPS_BAUDRATE is specified in variant (i.e. not 9600), skip to the specified rate. + if (speedSelect == 0 && GPS_BAUDRATE != serialSpeeds[speedSelect]) { + speedSelect = std::find(serialSpeeds, std::end(serialSpeeds), GPS_BAUDRATE) - serialSpeeds; + } + + LOG_DEBUG("Probing for GPS at %d", serialSpeeds[speedSelect]); + gnssModel = probe(serialSpeeds[speedSelect]); + if (gnssModel == GNSS_MODEL_UNKNOWN) { + if (++speedSelect == sizeof(serialSpeeds) / sizeof(int)) { + speedSelect = 0; + if (--probeTries == 0) { + LOG_WARN("Giving up on GPS probe and setting to 9600."); + return true; + } + } + return false; + } + } else { + gnssModel = GNSS_MODEL_UNKNOWN; + } + + 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_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 Configuration"); + } + + // 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)) { + + _serial_gps->write("$PAIR066,1,0,1,0,0,1*3B\r\n"); // Enable GPS+GALILEO+NAVIC + + // 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 powersaving ECO mode for Neo-6", 500); + SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersaving 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 configuration."); + } else { + LOG_INFO("GNSS module configuration saved!"); + } + } else if (IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9)) { + if (gnssModel == GNSS_MODEL_UBLOX7) { + LOG_DEBUG("Setting 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_INFO("reconfigure GNSS - defaults maintained. Is this module GPS-only?"); + } else { + if (gnssModel == GNSS_MODEL_UBLOX7) { + LOG_INFO("GNSS configured for GPS+SBAS."); + } else { // 8,9 + LOG_INFO("GNSS configured for GPS+SBAS+GLONASS+Galileo."); + } + // 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 (uBloxProtocolVersion >= 18) { + clearBuffer(); + SEND_UBX_PACKET(0x06, 0x86, _message_PMS, "enable powersaving for GPS", 500); + SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersaving 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 powersaving mode for GPS", 500); + SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersaving 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 configuration."); + } 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 powersaving for M10 GPS RAM", 300); + delay(750); + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_PM_BBR, "enable powersaving for M10 GPS BBR", 300); + delay(750); + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_RAM, "enable Jamming detection M10 GPS RAM", 300); + delay(750); + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_BBR, "enable Jamming 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 configuration."); + } else { + LOG_INFO("GNSS module configuration saved!"); + } + } + didSerialInit = true; + } + + notifyDeepSleepObserver.observe(¬ifyDeepSleep); + + return true; +} + +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 moving from %s to %s", getGPSPowerStateString(oldState), getGPSPowerStateString(newState)); + +#ifdef HELTEC_MESH_NODE_T114 + if ((oldState == GPS_OFF || oldState == GPS_HARDSLEEP) && (newState != GPS_OFF && newState != GPS_HARDSLEEP)) { + _serial_gps->begin(serialSpeeds[speedSelect]); + } else if ((newState == GPS_OFF || newState == GPS_HARDSLEEP) && (oldState != GPS_OFF && oldState != GPS_HARDSLEEP)) { + _serial_gps->end(); + } +#endif + 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_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 + if (config.position.gps_update_interval * 1000 >= GPS_FIX_HOLD_TIME * 2) { + digitalWrite(PIN_GPS_EN, LOW); + } +#endif + 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 +#ifdef GNSS_AIROHA + if (config.position.gps_update_interval * 1000 >= GPS_FIX_HOLD_TIME * 2) { + digitalWrite(PIN_GPS_EN, LOW); + } +#endif + 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 && (rotaryEncoderInterruptImpl1 || upDownInterruptImpl1)) + return; + + // Write and log + enablePin->set(on); +#ifdef GPS_EXTRAVERBOSE + LOG_DEBUG("Pin EN %s", val == HIGH ? "HIGH" : "LOW"); +#endif +} + +// Set the value of the STANDBY pin, if relevant +// true for standby state, false for awake +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; +#else + bool val = !standby; +#endif + + // Write and log + pinMode(PIN_GPS_STANDBY, OUTPUT); + digitalWrite(PIN_GPS_STANDBY, val); +#ifdef GPS_EXTRAVERBOSE + LOG_DEBUG("Pin STANDBY %s", val == HIGH ? "HIGH" : "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. +#ifdef HAS_PMU + // Abort: if no PMU + if (!pmu_found) + 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); + } + +#ifdef GPS_EXTRAVERBOSE + 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; + + // If waking + if (on) { + gps->_serial_gps->write(0xFF); + clearBuffer(); // This often returns old data, so drop it +#ifdef GPS_EXTRAVERBOSE + LOG_DEBUG("UBLOX: wake"); +#endif + } + + // 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); + } + + // Determine hardware version + if (gnssModel != GNSS_MODEL_UBLOX10) { + // Encode the sleep time in millis into the packet + for (int i = 0; i < 4; i++) + gps->_message_PMREQ[0 + i] = sleepMs >> (i * 8); + + // Record the message length + msglen = gps->makeUBXPacket(0x02, 0x41, sizeof(_message_PMREQ), gps->_message_PMREQ); + } else { + // Encode the sleep time in millis into the packet + for (int i = 0; i < 4; i++) + gps->_message_PMREQ_10[4 + i] = sleepMs >> (i * 8); + + // Record the message length + msglen = gps->makeUBXPacket(0x02, 0x41, sizeof(_message_PMREQ_10), gps->_message_PMREQ_10); + } + + // Send the UBX packet + gps->_serial_gps->write(gps->UBXscratch, msglen); + +#ifdef GPS_EXTRAVERBOSE + LOG_DEBUG("UBLOX: sleep for %dmS", sleepMs); +#endif + } +} + +/// Record that we have a GPS +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); +} + +// 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); + + LOG_DEBUG("%us until next search", sleepTime / 1000); + + // If update interval less than 10 seconds, no attempt to sleep + if (updateInterval <= 10 * 1000UL || sleepTime == 0) + setPowerState(GPS_IDLE); + + else { + // Check whether the GPS hardware is capable of GPS_SOFTSLEEP + // If not, fallback to GPS_HARDSLEEP instead + bool softsleepSupported = false; + // 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; +#ifdef PIN_GPS_STANDBY // L76B, L76K and clones have a standby pin + softsleepSupported = true; +#endif + + 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 impromevement 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); + } +} + +void GPS::publishUpdate() +{ + if (shouldPublish) { + shouldPublish = false; + + // In debug logs, identify position by @timestamp:stage (stage 2 = publish) + LOG_DEBUG("publishing 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(); + } + } +} + +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. Skipping probe."); + return disable(); + } + if (!setup()) + return 2000; // 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(); + } + // ONCE we will factory reset the GPS for bug #327 + if (!devicestate.did_gps_reset) { + LOG_WARN("GPS FactoryReset requested"); + if (gps->factoryReset()) { // If we don't succeed try again next time + devicestate.did_gps_reset = true; + nodeDB->saveToDisk(SEGMENT_DEVICESTATE); + } + } + GPSInitFinished = true; + } + + // Repeaters have no need for GPS + if (config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) { + return disable(); + } + + if (whileActive()) { + // if we have received valid NMEA claim we are connected + setConnected(); + } else { + if ((config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) && + IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX6, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9, + GNSS_MODEL_UBLOX10)) { + // reset the GPS on next bootup + if (devicestate.did_gps_reset && scheduling.elapsedSearchMs() > 60 * 1000UL && !hasFlow()) { + LOG_DEBUG("GPS is not communicating, trying factory reset on next bootup."); + devicestate.did_gps_reset = false; + nodeDB->saveToDisk(SEGMENT_DEVICESTATE); + return disable(); // Stop the GPS thread as it can do nothing useful until next reboot. + } + } + } + // At least one GPS has a bad habit of losing its mind from time to time + if (rebootsSeen > 2) { + rebootsSeen = 0; + LOG_DEBUG("Would normally factoryReset()"); + // gps->factoryReset(); + } + + // If we're due for an update, wake the GPS + if (!config.position.fixed_position && powerState != GPS_ACTIVE && scheduling.isUpdateDue()) + up(); + + // If we've already set time from the GPS, no need to ask the GPS + bool gotTime = (getRTCQuality() >= RTCQualityGPS); + if (!gotTime && lookForTime()) { // Note: we count on this && short-circuiting and not resetting the RTC time + gotTime = true; + shouldPublish = true; + } + + bool gotLoc = lookForLocation(); + if (gotLoc && !hasValidLocation) { // declare that we have location ASAP + LOG_DEBUG("hasValidLocation RISING EDGE"); + hasValidLocation = true; + shouldPublish = true; + } + + bool tooLong = scheduling.searchedTooLong(); + if (tooLong) + LOG_WARN("Couldn't publish a valid location: didn't get a GPS lock in time."); + + // Once we get a location we no longer desperately want an update + // LOG_DEBUG("gotLoc %d, tooLong %d, gotTime %d", gotLoc, tooLong, gotTime); + if ((gotLoc && gotTime) || tooLong) { + + if (tooLong) { + // we didn't get a location during this ack window, therefore declare loss of lock + if (hasValidLocation) { + LOG_DEBUG("hasValidLocation FALLING EDGE"); + } + p = meshtastic_Position_init_default; + hasValidLocation = false; + } + + down(); + shouldPublish = true; // publish our update for this just finished acquisition window + } + + // If state has changed do a publish + publishUpdate(); + + 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; +} + +// clear the GPS rx buffer as quickly as possible +void GPS::clearBuffer() +{ + int x = _serial_gps->available(); + while (x--) + _serial_gps->read(); +} + +/// 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; +} + +const char *PROBE_MESSAGE = "Trying %s (%s)..."; +const char *DETECTED_MESSAGE = "%s detected, using %s Module"; + +#define PROBE_SIMPLE(CHIP, TOWRITE, RESPONSE, DRIVER, TIMEOUT, ...) \ + 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, #DRIVER); \ + return DRIVER; \ + } + +GnssModel_t GPS::probe(int serialSpeed) +{ +#if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL) + _serial_gps->end(); + _serial_gps->begin(serialSpeed); +#elif defined(ARCH_RP2040) + _serial_gps->end(); + _serial_gps->setFIFOSize(256); + _serial_gps->begin(serialSpeed); +#else + if (_serial_gps->baudRate() != serialSpeed) { + LOG_DEBUG("Setting Baud to %i", serialSpeed); + _serial_gps->updateBaudRate(serialSpeed); + } +#endif + + memset(&info, 0, sizeof(struct uBloxGnssModelInfo)); + uint8_t buffer[768] = {0}; + delay(100); + + // 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); + + // Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A + PROBE_SIMPLE("UC6580", "$PDTINFO", "UC6580", GNSS_MODEL_UC6580, 500); + PROBE_SIMPLE("UM600", "$PDTINFO", "UM600", GNSS_MODEL_UC6580, 500); + PROBE_SIMPLE("ATGM336H", "$PCAS06,1*1A", "$GPTXT,01,01,02,HW=ATGM336H", GNSS_MODEL_ATGM336H, 500); + /* ATGM332D series (-11(GPS), -21(BDS), -31(GPS+BDS), -51(GPS+GLONASS), -71-0(GPS+BDS+GLONASS)) + based on AT6558 */ + PROBE_SIMPLE("ATGM332D", "$PCAS06,1*1A", "$GPTXT,01,01,02,HW=ATGM332D", GNSS_MODEL_ATGM336H, 500); + + /* 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 + PROBE_SIMPLE("AG3335", "$PAIR021*39", "$PAIR021,AG3335", GNSS_MODEL_AG3335, 500); + PROBE_SIMPLE("AG3352", "$PAIR021*39", "$PAIR021,AG3352", GNSS_MODEL_AG3352, 500); + 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); + + // Close all NMEA sentences, valid for L76B MTK platform (Waveshare Pico GPS) + _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); + + PROBE_SIMPLE("L76B", "$PMTK605*31", "Quectel-L76B", GNSS_MODEL_MTK_L76B, 500); + + 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("Failed to find GNSS Module (baudrate %d)", serialSpeed); + 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) { + // LOG_DEBUG("monver reply size = %d", len); + uint16_t position = 0; + for (int i = 0; i < 30; i++) { + info.swVersion[i] = buffer[position]; + position++; + } + for (int i = 0; i < 10; i++) { + info.hwVersion[i] = buffer[position]; + position++; + } + + while (len >= position + 30) { + for (int i = 0; i < 30; i++) { + info.extension[info.extensionNo][i] = buffer[position]; + position++; + } + info.extensionNo++; + if (info.extensionNo > 9) + break; + } + + LOG_DEBUG("Module Info : "); + LOG_DEBUG("Soft version: %s", info.swVersion); + LOG_DEBUG("Hard version: %s", info.hwVersion); + LOG_DEBUG("Extensions:%d", info.extensionNo); + for (int i = 0; i < info.extensionNo; i++) { + LOG_DEBUG(" %s", info.extension[i]); + } + + memset(buffer, 0, sizeof(buffer)); + + // tips: extensionNo field is 0 on some 6M GNSS modules + for (int i = 0; i < info.extensionNo; ++i) { + if (!strncmp(info.extension[i], "MOD=", 4)) { + strncpy((char *)buffer, &(info.extension[i][4]), sizeof(buffer)); + } else if (!strncmp(info.extension[i], "PROTVER", 7)) { + char *ptr = nullptr; + memset(buffer, 0, sizeof(buffer)); + strncpy((char *)buffer, &(info.extension[i][8]), sizeof(buffer)); + LOG_DEBUG("Protocol Version:%s", (char *)buffer); + if (strlen((char *)buffer)) { + uBloxProtocolVersion = strtoul((char *)buffer, &ptr, 10); + LOG_DEBUG("ProtVer=%d", uBloxProtocolVersion); + } else { + uBloxProtocolVersion = 0; + } + } + } + if (strncmp(info.hwVersion, "00040007", 8) == 0) { + LOG_INFO(DETECTED_MESSAGE, "U-blox 6", "6"); + return GNSS_MODEL_UBLOX6; + } else if (strncmp(info.hwVersion, "00070000", 8) == 0) { + LOG_INFO(DETECTED_MESSAGE, "U-blox 7", "7"); + return GNSS_MODEL_UBLOX7; + } else if (strncmp(info.hwVersion, "00080000", 8) == 0) { + LOG_INFO(DETECTED_MESSAGE, "U-blox 8", "8"); + return GNSS_MODEL_UBLOX8; + } else if (strncmp(info.hwVersion, "00190000", 8) == 0) { + LOG_INFO(DETECTED_MESSAGE, "U-blox 9", "9"); + return GNSS_MODEL_UBLOX9; + } else if (strncmp(info.hwVersion, "000A0000", 8) == 0) { + LOG_INFO(DETECTED_MESSAGE, "U-blox 10", "10"); + return GNSS_MODEL_UBLOX10; + } + } + LOG_WARN("Failed to find GNSS Module (baudrate %d)", serialSpeed); + 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; +#if HAS_GPS && !defined(ARCH_ESP32) + _rx_gpio = 1; // We only specify GPS serial ports on ESP32. Otherwise, these are just flags. + _tx_gpio = 1; +#endif +#if defined(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; +#endif +#if defined(PIN_GPS_EN) + if (!_en_gpio) + _en_gpio = PIN_GPS_EN; +#endif +#ifdef ARCH_PORTDUINO + if (!settingsMap[has_gps]) + return nullptr; +#endif + 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; + + 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 + } + } + +#ifdef PIN_GPS_PPS + // pulse per second + pinMode(PIN_GPS_PPS, INPUT); +#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("Using " NMEA_MSG_GXGSA " for 3DFIX and PDOP"); +#endif + + // 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); // assert for 10ms + delay(10); + digitalWrite(PIN_GPS_RESET, !GPS_RESET_MODE); +#endif + + 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 +#endif + +// ESP32 has a special set of parameters vs other arduino ports +#if defined(ARCH_ESP32) + LOG_DEBUG("Using GPIO%d for GPS RX", new_gps->rx_gpio); + LOG_DEBUG("Using GPIO%d for GPS TX", 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->setFIFOSize(256); + _serial_gps->begin(GPS_BAUDRATE); +#else + _serial_gps->begin(GPS_BAUDRATE); +#endif + } + 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; +} + +bool GPS::factoryReset() +{ +#ifdef PIN_GPS_REINIT + // The L76K GNSS on the T-Echo requires the RESET pin to be pulled LOW + pinMode(PIN_GPS_REINIT, OUTPUT); + digitalWrite(PIN_GPS_REINIT, 0); + delay(150); // The L76K datasheet calls for at least 100MS delay + digitalWrite(PIN_GPS_REINIT, 1); +#endif + + if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) { + byte _message_reset1[] = {0xB5, 0x62, 0x06, 0x09, 0x0D, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x1C, 0xA2}; + _serial_gps->write(_message_reset1, sizeof(_message_reset1)); + if (getACK(0x05, 0x01, 10000)) { + LOG_INFO(ACK_SUCCESS_MESSAGE); + } + delay(100); + byte _message_reset2[] = {0xB5, 0x62, 0x06, 0x09, 0x0D, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x1B, 0xA1}; + _serial_gps->write(_message_reset2, sizeof(_message_reset2)); + if (getACK(0x05, 0x01, 10000)) { + LOG_INFO(ACK_SUCCESS_MESSAGE); + } + delay(100); + byte _message_reset3[] = {0xB5, 0x62, 0x06, 0x09, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x03, 0x1D, 0xB3}; + _serial_gps->write(_message_reset3, sizeof(_message_reset3)); + if (getACK(0x05, 0x01, 10000)) { + LOG_INFO(ACK_SUCCESS_MESSAGE); + } + // Reset device ram to COLDSTART state + // byte _message_CFG_RST_COLDSTART[] = {0xB5, 0x62, 0x06, 0x04, 0x04, 0x00, 0xFF, 0xB9, 0x00, 0x00, 0xC6, 0x8B}; + // _serial_gps->write(_message_CFG_RST_COLDSTART, sizeof(_message_CFG_RST_COLDSTART)); + // delay(1000); + } else if (gnssModel == GNSS_MODEL_MTK) { + // send the CAS10 to perform a factory restart of the device (and other device that support PCAS statements) + LOG_INFO("GNSS Factory Reset via PCAS10,3"); + _serial_gps->write("$PCAS10,3*1F\r\n"); + delay(100); + } else if (gnssModel == GNSS_MODEL_ATGM336H) { + LOG_INFO("Factory Reset via CAS-CFG-RST"); + uint8_t msglen = makeCASPacket(0x06, 0x02, sizeof(_message_CAS_CFG_RST_FACTORY), _message_CAS_CFG_RST_FACTORY); + _serial_gps->write(UBXscratch, msglen); + delay(100); + } else { + // fire this for good measure, if we have an L76B - won't harm other devices. + _serial_gps->write("$PMTK104*37\r\n"); + // No PMTK_ACK for this command. + delay(100); + // send the UBLOX Factory Reset Command regardless of detect state, something is very wrong, just assume it's UBLOX. + // Factory Reset + byte _message_reset[] = {0xB5, 0x62, 0x06, 0x09, 0x0D, 0x00, 0xFF, 0xFB, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x17, 0x2B, 0x7E}; + _serial_gps->write(_message_reset, sizeof(_message_reset)); + } + delay(1000); + return true; +} + +/** + * 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 + */ +bool GPS::lookForTime() +{ + +#ifdef GNSS_AIROHA + uint8_t fix = reader.fixQuality(); + if (fix > 0) { + if (lastFixStartMsec > 0) { + if (Throttle::isWithinTimespanMs(lastFixStartMsec, GPS_FIX_HOLD_TIME)) { + return false; + } else { + clearBuffer(); + } + } else { + lastFixStartMsec = millis(); + return false; + } + } else { + return false; + } +#endif + 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) { + LOG_DEBUG("NMEA GPS time %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()); + perhapsSetRTC(RTCQualityGPS, t); + return true; + } else + return false; + } else + return false; +} + +/** + * 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 + */ +bool GPS::lookForLocation() +{ +#ifdef GNSS_AIROHA + if ((config.position.gps_update_interval * 1000) >= (GPS_FIX_HOLD_TIME * 2)) { + uint8_t fix = reader.fixQuality(); + if (fix > 0) { + if (lastFixStartMsec > 0) { + if (Throttle::isWithinTimespanMs(lastFixStartMsec, GPS_FIX_HOLD_TIME)) { + return false; + } else { + clearBuffer(); + } + } else { + lastFixStartMsec = millis(); + return false; + } + } else { + return false; + } + } +#endif + // 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) { + 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 + // LOG_DEBUG("FIX QUAL=%d, TYPE=%d", fixQual, fixType); +#endif + + // check if GPS has an acceptable lock + if (!hasLock()) + return false; + +#ifdef GPS_EXTRAVERBOSE + LOG_DEBUG("AGE: LOC=%d FIX=%d DATE=%d TIME=%d", reader.location.age(), +#ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS + gsafixtype.age(), +#else + 0, +#endif + reader.date.age(), reader.time.age()); +#endif // GPS_EXTRAVERBOSE + + // 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_EXTRAVERBOSE + LOG_DEBUG("Bail out EARLY on LAT %i", toDegInt(loc.lat)); +#endif + return false; + } + if (toDegInt(loc.lng) > 1800000000) { +#ifdef GPS_EXTRAVERBOSE + 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()); + // LOG_DEBUG("PDOP=%d, HDOP=%d", p.PDOP, p.HDOP); +#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. Flushing to avoid corruption.", _serial_gps->available()); + clearBuffer(); + } +#endif + // if (_serial_gps->available() > 0) + // LOG_DEBUG("GPS Bytes Waiting: %u", _serial_gps->available()); + // 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 + LOG_DEBUG(debugmsg.c_str()); +#endif + return isValid; +} +void GPS::enable() +{ + // Clear the old scheduling info (reset the lock-time prediction) + scheduling.reset(); + + enabled = true; + setInterval(GPS_THREAD_INTERVAL); + + scheduling.informSearching(); + setPowerState(GPS_ACTIVE); +} + +int32_t GPS::disable() +{ + enabled = false; + setInterval(INT32_MAX); + setPowerState(GPS_OFF); + + 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(); +#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(); + } +} +#endif // Exclude GPS \ No newline at end of file diff --git a/src/gps/GPS.h b/src/gps/GPS.h new file mode 100644 index 0000000..8b1982c --- /dev/null +++ b/src/gps/GPS.h @@ -0,0 +1,320 @@ +#pragma once +#include "configuration.h" +#if !MESHTASTIC_EXCLUDE_GPS + +#include "GPSStatus.h" +#include "GpioLogic.h" +#include "Observer.h" +#include "TinyGPS++.h" +#include "concurrency/OSThread.h" +#include "input/RotaryEncoderInterruptImpl1.h" +#include "input/UpDownInterruptImpl1.h" +#include "modules/PositionModule.h" + +// Allow defining the polarity of the ENABLE output. default is active high +#ifndef GPS_EN_ACTIVE +#define GPS_EN_ACTIVE 1 +#endif + +struct uBloxGnssModelInfo { + char swVersion[30]; + char hwVersion[10]; + uint8_t extensionNo; + char extension[10][30]; +}; + +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_AG3335, + GNSS_MODEL_AG3352 +} GnssModel_t; + +typedef enum { + 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 +}; + +// Generate a string representation of DOP +const char *getDOPString(uint32_t dop); + +/** + * 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 +{ + TinyGPSPlus reader; + uint8_t fixQual = 0; // fix quality from GPGGA + uint32_t lastChecksumFailCount = 0; + +#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 +#endif + private: + const int serialSpeeds[6] = {9600, 115200, 38400, 4800, 57600, 9600}; + uint32_t lastWakeStartMsec = 0, lastSleepStartMsec = 0, lastFixStartMsec = 0; + uint32_t rx_gpio = 0; + uint32_t tx_gpio = 0; + + int speedSelect = 0; + int probeTries = 2; + + /** + * 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 isInPowersave = false; + + 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 GPSInitFinished = false; // Init thread finished? + bool GPSInitStarted = false; // Init thread finished? + + GPSPowerState powerState = GPS_OFF; // GPS_ACTIVE if we want a location right now + + uint8_t numSatellites = 0; + + CallbackObserver notifyDeepSleepObserver = CallbackObserver(this, &GPS::prepareDeepSleep); + + public: + /** If !NULL we will use this serial port to construct our GPS */ +#if defined(ARCH_RP2040) + static SerialUART *_serial_gps; +#else + static HardwareSerial *_serial_gps; +#endif + static uint8_t _message_PMREQ[]; + static uint8_t _message_PMREQ_10[]; + static const uint8_t _message_CFG_RXM_PSM[]; + static const uint8_t _message_CFG_RXM_ECO[]; + static const uint8_t _message_CFG_PM2[]; + static const uint8_t _message_GNSS_7[]; + static const uint8_t _message_GNSS_8[]; + static const uint8_t _message_JAM_6_7[]; + static const uint8_t _message_JAM_8[]; + static const uint8_t _message_NAVX5[]; + static const uint8_t _message_NAVX5_8[]; + static const uint8_t _message_NMEA[]; + static const uint8_t _message_DISABLE_TXT_INFO[]; + static const uint8_t _message_1HZ[]; + static const uint8_t _message_GLL[]; + static const uint8_t _message_GSA[]; + static const uint8_t _message_GSV[]; + static const uint8_t _message_VTG[]; + static const uint8_t _message_RMC[]; + static const uint8_t _message_AID[]; + static const uint8_t _message_GGA[]; + static const uint8_t _message_PMS[]; + static const uint8_t _message_SAVE[]; + static const uint8_t _message_SAVE_10[]; + + // VALSET Commands for M10 + static const uint8_t _message_VALSET_PM[]; + static const uint8_t _message_VALSET_PM_RAM[]; + static const uint8_t _message_VALSET_PM_BBR[]; + static const uint8_t _message_VALSET_ITFM_RAM[]; + static const uint8_t _message_VALSET_ITFM_BBR[]; + static const uint8_t _message_VALSET_DISABLE_NMEA_RAM[]; + static const uint8_t _message_VALSET_DISABLE_NMEA_BBR[]; + static const uint8_t _message_VALSET_DISABLE_TXT_INFO_RAM[]; + static const uint8_t _message_VALSET_DISABLE_TXT_INFO_BBR[]; + static const uint8_t _message_VALSET_ENABLE_NMEA_RAM[]; + static const uint8_t _message_VALSET_ENABLE_NMEA_BBR[]; + static const uint8_t _message_VALSET_DISABLE_SBAS_RAM[]; + static const uint8_t _message_VALSET_DISABLE_SBAS_BBR[]; + + // CASIC commands for ATGM336H + static const uint8_t _message_CAS_CFG_RST_FACTORY[]; + static const uint8_t _message_CAS_CFG_NAVX_CONF[]; + static const uint8_t _message_CAS_CFG_RATE_1HZ[]; + + const char *ACK_SUCCESS_MESSAGE = "Get ack success!"; + + 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; + + GPS() : concurrency::OSThread("GPS") {} + + virtual ~GPS(); + + /** We will notify this observable anytime GPS state has changed meaningfully */ + Observable newStatus; + + /** + * Returns true if we succeeded + */ + virtual bool setup(); + + // re-enable the thread + void enable(); + + // Disable the thread + int32_t disable() override; + + // 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); + + /// 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(); + + /// 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; } + + // Empty the input buffer as quickly as possible + void clearBuffer(); + + // 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}; + + 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); + + GPS_RESPONSE getACKCas(uint8_t class_id, uint8_t msg_id, uint32_t waitMillis); + + virtual bool factoryReset(); + + // 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(); + + // Let the GPS hardware save power between updates + void down(); + + protected: + /** + * 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 + */ + + /** + * 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 + */ + + /// 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(); + + /** + * 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(); + + private: + /// 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); + + // Calculate checksum + void UBXChecksum(uint8_t *message, size_t length); + void CASChecksum(uint8_t *message, size_t length); + + /** 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 GPS power with PMU, if relevant + */ + void setPowerPMU(bool on); + + /** Set UBLOX power, if relevant + */ + void setPowerUBLOX(bool on, uint32_t sleepMs = 0); + + /** + * Tell users we have new GPS readings + */ + void publishUpdate(); + + virtual int32_t runOnce() override; + + // Get GNSS model + GnssModel_t probe(int serialSpeed); + + // delay counter to allow more sats before fixed position stops GPS thread + uint8_t fixeddelayCtr = 0; + + const char *powerStateToString(); + + protected: + GnssModel_t gnssModel = GNSS_MODEL_UNKNOWN; +}; + +extern GPS *gps; +#endif // Exclude GPS \ No newline at end of file diff --git a/src/gps/GPSUpdateScheduling.cpp b/src/gps/GPSUpdateScheduling.cpp new file mode 100644 index 0000000..abcf6b1 --- /dev/null +++ b/src/gps/GPSUpdateScheduling.cpp @@ -0,0 +1,118 @@ +#include "GPSUpdateScheduling.h" + +#include "Default.h" + +// Mark the time when searching for GPS position begins +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(); +} + +// Clear old lock-time prediction data. +// When re-enabling GPS with user button. +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(); + + // 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; + + // If we should have already started (negative value), start ASAP + if (remainingMs < 0) + remainingMs = 0; + + return (uint32_t)remainingMs; +} + +// How long have we already been searching? +// Used to abort a search in progress, if it runs unnaceptably long +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; +} + +// Is it now time to begin searching for a GPS position? +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; + + // 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; +} + +// Updates the predicted time-to-get-lock, by exponentially smoothing the latest observation +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; + + // Ignore the first lock-time: likely to be long, will skew data + + // Second locktime: likely stable. Use to intialize 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)); + + searchCount++; // Only tracked so we can diregard initial lock-times + + LOG_DEBUG("Predicting %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; +} diff --git a/src/gps/GPSUpdateScheduling.h b/src/gps/GPSUpdateScheduling.h new file mode 100644 index 0000000..7e121c9 --- /dev/null +++ b/src/gps/GPSUpdateScheduling.h @@ -0,0 +1,29 @@ +#pragma once + +#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 + + 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? + + 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". +}; \ No newline at end of file diff --git a/src/gps/GeoCoord.cpp b/src/gps/GeoCoord.cpp new file mode 100644 index 0000000..5abb25a --- /dev/null +++ b/src/gps/GeoCoord.cpp @@ -0,0 +1,576 @@ +#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(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::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 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'; + + double latDeg = lat; + + 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; + + if (lon < 0) + dms.lonCP = 'W'; + else + dms.lonCP = 'E'; + + double lonDeg = lon; + + 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; +} + +/** + * 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) +{ + + 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; + } + + 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 +} + +// 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; +} + +/** + * 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); + + 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 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); + } +} + +/** + * 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)); + + 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; + } + } else { + lat_val /= pow(5, 5); + lng_val /= pow(4, 5); + } + + 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; + + 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] = '+'; + } + + 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); + + // 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 +} + +/// 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; + + 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); +} + +/** + * Computes the bearing in degrees between two points on Earth. Ported from my + * old Gaggle android app. + * + * @param lat1 + * Latitude of the first point + * @param lon1 + * Longitude of the first point + * @param lat2 + * Latitude of the second point + * @param lon2 + * Longitude of the second point + * @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); +} + +/** + * Ported from http://www.edwilliams.org/avform147.htm#Intro + * @brief Convert from meters to range in radians on a great circle + * @param range_meters + * 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; +} + +/** + * Ported from http://www.edwilliams.org/avform147.htm#Intro + * @brief Convert from radians to range in meters on a great circle + * @param range_radians + * 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; +} + +// 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); +} + +// 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); +} + +/** + * Create a new point bassed on the passed in poin + * Ported from http://www.edwilliams.org/avform147.htm#LL + * @param bearing + * The bearing in raidans + * @param range_meters + * 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; + + return std::make_shared(double(lat), double(lon), this->getAltitude()); +} + +/** + * Convert bearing to degrees + * @param bearing + * 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; +} + +/** + * Convert bearing to string + * @param degrees + * 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"; +} diff --git a/src/gps/GeoCoord.h b/src/gps/GeoCoord.h new file mode 100644 index 0000000..ecdaf0e --- /dev/null +++ b/src/gps/GeoCoord.h @@ -0,0 +1,165 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#define PI 3.1415926535897932384626433832795 +#define OLC_CODE_LEN 11 +#define DEG_CONVERT (180 / PI) + +// Helper functions +// Raises a number to an exponent, handling negative exponents. +static inline double pow_neg(double base, double exponent) +{ + if (exponent == 0) { + return 1; + } else if (exponent > 0) { + return pow(base, exponent); + } + return 1 / pow(base, -exponent); +} + +static inline double toRadians(double deg) +{ + return deg * PI / 180; +} + +static inline double toDegrees(double r) +{ + return r * 180 / PI; +} + +// 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; +}; + +// 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; +}; + +// 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; +}; + +// A struct to hold the data for a OSGR coordinate +struct OSGR { + 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 +}; + +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 = {}; + + bool _dirty = true; + + 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); + + 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); + + // 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; } + + // 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; } + + // 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; } + + // OLC getter + void getOLCCode(char *code) { strncpy(code, _olc.code, OLC_CODE_LEN + 1); } // +1 for null termination +}; diff --git a/src/gps/NMEAWPL.cpp b/src/gps/NMEAWPL.cpp new file mode 100644 index 0000000..f528c46 --- /dev/null +++ b/src/gps/NMEAWPL.cpp @@ -0,0 +1,102 @@ +#if !MESHTASTIC_EXCLUDE_GPS +#include "NMEAWPL.h" +#include "GeoCoord.h" +#include "RTC.h" +#include + +/* ------------------------------------------- + * 1 2 3 4 5 6 + * | | | | | | + * $--WPL,llll.ll,a,yyyyy.yy,a,c--c*hh + * + * Field Number: + * 1 Latitude + * 2 N or S (North or South) + * 3 Longitude + * 4 E or W (East or West) + * 5 Waypoint name + * 6 Checksum + * ------------------------------------------- + */ + +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, "$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 + * | | | | | | | | | | | | | | | + * $--GGA,hhmmss.ss,ddmm.mm,a,ddmm.mm,a,x,xx,x.x,x.x,M,x.x,M,x.x,xxxx*hh + * + * Field Number: + * 1 UTC of this position report, hh is hours, mm is minutes, ss.ss is seconds. + * 2 Latitude + * 3 N or S (North or South) + * 4 Longitude + * 5 E or W (East or West) + * 6 GPS Quality Indicator (non null) + * 7 Number of satellites in use, 00 - 12 + * 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 + * ------------------------------------------- + */ + +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); + } + + 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; +} + +#endif \ No newline at end of file diff --git a/src/gps/NMEAWPL.h b/src/gps/NMEAWPL.h new file mode 100644 index 0000000..5821624 --- /dev/null +++ b/src/gps/NMEAWPL.h @@ -0,0 +1,8 @@ +#pragma once + +#include "main.h" +#include + +uint32_t printWPL(char *buf, size_t bufsz, const meshtastic_Position &pos, const char *name, bool isCaltopoMode = false); +uint32_t printWPL(char *buf, size_t bufsz, const meshtastic_PositionLite &pos, const char *name, bool isCaltopoMode = false); +uint32_t printGGA(char *buf, size_t bufsz, const meshtastic_Position &pos); \ No newline at end of file diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp new file mode 100644 index 0000000..8130d76 --- /dev/null +++ b/src/gps/RTC.cpp @@ -0,0 +1,293 @@ +#include "RTC.h" +#include "configuration.h" +#include "detect/ScanI2C.h" +#include "main.h" +#include +#include +#include + +static RTCQuality currentQuality = RTCQualityNone; +uint32_t lastSetFromPhoneNtpOrGps = 0; + +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 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. + */ +void 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 WIRE_INTERFACES_COUNT == 2 + rtc.initI2C(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); +#else + 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 + 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); + timeStartMsec = now; + zeroOffsetSecs = tv.tv_sec; + if (currentQuality == RTCQualityNone) { + currentQuality = RTCQualityDevice; + } + } +#elif defined(PCF8563_RTC) + if (rtc_found.address == PCF8563_RTC) { + uint32_t now = millis(); + PCF8563_Class rtc; + +#if WIRE_INTERFACES_COUNT == 2 + rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); +#else + rtc.begin(); +#endif + + auto tc = rtc.getDateTime(); + tm t; + t.tm_year = tc.year - 1900; + t.tm_mon = tc.month - 1; + t.tm_mday = tc.day; + t.tm_hour = tc.hour; + t.tm_min = tc.minute; + t.tm_sec = tc.second; + 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 PCF8563 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); + timeStartMsec = now; + zeroOffsetSecs = tv.tv_sec; + if (currentQuality == RTCQualityNone) { + currentQuality = RTCQualityDevice; + } + } +#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; + } +#endif +} + +/** + * Sets the RTC (Real-Time Clock) if the provided time is of higher quality than the current RTC time. + * + * @param q The quality of the provided time. + * @param tv A pointer to a timeval struct containing the time to potentially set the RTC to. + * @return True if the RTC was set, false otherwise. + * + * If we haven't yet set our RTC this boot, set it from a GPS derived time + */ +bool 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) { + LOG_WARN("Ignoring time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + return false; + } +#endif + + bool shouldSet; + if (forceUpdate) { + shouldSet = true; + LOG_DEBUG("Overriding current RTC quality (%s) with incoming time of RTC quality of %s", RtcName(currentQuality), + RtcName(q)); + } else if (q > currentQuality) { + shouldSet = true; + LOG_DEBUG("Upgrading time to quality %s", RtcName(q)); + } else if (q == RTCQualityGPS) { + shouldSet = true; + LOG_DEBUG("Reapplying 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("Reapplying external time to correct clock drift %ld secs", printableEpoch); + } else { + shouldSet = false; + LOG_DEBUG("Current RTC quality: %s. Ignoring 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); + } +#elif defined(PCF8563_RTC) + if (rtc_found.address == PCF8563_RTC) { + PCF8563_Class rtc; + +#if WIRE_INTERFACES_COUNT == 2 + rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); +#else + rtc.begin(); +#endif + tm *t = gmtime(&tv->tv_sec); + rtc.setDateTime(t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); + LOG_DEBUG("PCF8563_RTC 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); + } +#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 true; + } else { + return false; + } +} + +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"; + } +} + +/** + * Sets the RTC time if the provided time is of higher quality than the current RTC time. + * + * @param q The quality of the provided time. + * @param t The time to potentially set the RTC to. + * @return True if the RTC was set to the provided time, false otherwise. + */ +bool 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); + + // 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("Ignoring invalid GPS month=%d, year=%d, unixtime=%ld", t.tm_mon, t.tm_year, tv.tv_sec); + return false; + } else { + return perhapsSetRTC(q, &tv); + } +} + +/** + * Returns the timezone offset in seconds. + * + * @return The timezone offset in seconds. + */ +int32_t getTZOffset() +{ + time_t now = getTime(false); + struct tm *gmt; + gmt = gmtime(&now); + gmt->tm_isdst = -1; + return (int32_t)difftime(now, mktime(gmt)); +} + +/** + * Returns the current time in seconds since the Unix epoch (January 1, 1970). + * + * @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; + } +} + +/** + * Returns the current time from the RTC if the quality of the time is at least minQuality. + * + * @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; +} + +time_t gm_mktime(struct tm *tm) +{ +#if !MESHTASTIC_EXCLUDE_TZ + setenv("TZ", "GMT0", 1); + time_t res = mktime(tm); + if (*config.device.tzdef) { + setenv("TZ", config.device.tzdef, 1); + } else { + setenv("TZ", "UTC0", 1); + } + return res; +#else + return mktime(tm); +#endif +} diff --git a/src/gps/RTC.h b/src/gps/RTC.h new file mode 100644 index 0000000..caa48dc --- /dev/null +++ b/src/gps/RTC.h @@ -0,0 +1,48 @@ +#pragma once + +#include "configuration.h" +#include "sys/time.h" +#include + +enum RTCQuality { + + /// We haven't had our RTC set yet + RTCQualityNone = 0, + + /// We got time from an onboard peripheral after boot. + RTCQualityDevice = 1, + + /// 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 our own GPS + RTCQualityGPS = 4 +}; + +RTCQuality getRTCQuality(); + +extern uint32_t lastSetFromPhoneNtpOrGps; + +/// If we haven't yet set our RTC this boot, set it from a GPS derived time +bool perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate = false); +bool perhapsSetRTC(RTCQuality q, struct tm &t); + +/// Return a string name for the quality +const char *RtcName(RTCQuality quality); + +/// Return time since 1970 in secs. While quality is RTCQualityNone we will be returning time based at zero +uint32_t getTime(bool local = false); + +/// Return time since 1970 in secs. If quality is RTCQualityNone return zero +uint32_t getValidTime(RTCQuality minQuality, bool local = false); + +void readFromRTC(); + +time_t gm_mktime(struct tm *tm); + +#define SEC_PER_DAY 86400 +#define SEC_PER_HOUR 3600 +#define SEC_PER_MIN 60 diff --git a/src/gps/cas.h b/src/gps/cas.h new file mode 100644 index 0000000..53d75cd --- /dev/null +++ b/src/gps/cas.h @@ -0,0 +1,63 @@ +#pragma once + +// CASIC binary message definitions +// Reference: https://www.icofchina.com/d/file/xiazai/2020-09-22/20f1b42b3a11ac52089caf3603b43fb5.pdf +// ATGM33H-5N: https://www.icofchina.com/pro/mokuai/2016-08-01/4.html +// (https://www.icofchina.com/d/file/xiazai/2016-12-05/b5c57074f4b1fcc62ba8c7868548d18a.pdf) + +// NEMA (Class ID - 0x4e) message IDs +#define CAS_NEMA_GGA 0x00 +#define CAS_NEMA_GLL 0x01 +#define CAS_NEMA_GSA 0x02 +#define CAS_NEMA_GSV 0x03 +#define CAS_NEMA_RMC 0x04 +#define CAS_NEMA_VTG 0x05 +#define CAS_NEMA_GST 0x07 +#define CAS_NEMA_ZDA 0x08 +#define CAS_NEMA_DHV 0x0D + +// Size of a CAS-ACK-(N)ACK message (14 bytes) +#define CAS_ACK_NACK_MSG_SIZE 0x0E + +// CFG-RST (0x06, 0x02) +// Factory reset +const uint8_t GPS::_message_CAS_CFG_RST_FACTORY[] = { + 0xFF, 0x03, // Fields to clear + 0x01, // Reset Mode: Controlled Software reset + 0x03 // Startup Mode: Factory +}; + +// CFG_RATE (0x06, 0x01) +// 1HZ update rate, this should always be the case after +// factory reset but update it regardless +const uint8_t GPS::_message_CAS_CFG_RATE_1HZ[] = { + 0xE8, 0x03, // Update Rate: 0x03E8 = 1000ms + 0x00, 0x00 // Reserved +}; + +// CFG-NAVX (0x06, 0x07) +// Initial ATGM33H-5N configuration, Updates for Dynamic Mode, Fix Mode, and SV system +// Qwirk: The ATGM33H-5N-31 should only support GPS+BDS, however it will happily enable +// and use GPS+BDS+GLONASS iff the correct CFG_NAVX command is used. +const uint8_t GPS::_message_CAS_CFG_NAVX_CONF[] = { + 0x03, 0x01, 0x00, 0x00, // Update Mask: Dynamic Mode, Fix Mode, Nav Settings + 0x03, // Dynamic Mode: Automotive + 0x03, // Fix Mode: Auto 2D/3D + 0x00, // Min SV + 0x00, // Max SVs + 0x00, // Min CNO + 0x00, // Reserved1 + 0x00, // Init 3D fix + 0x00, // Min Elevation + 0x00, // Dr Limit + 0x07, // Nav System: 2^0 = GPS, 2^1 = BDS 2^2 = GLONASS: 2^3 + // 3=GPS+BDS, 7=GPS+BDS+GLONASS + 0x00, 0x00, // Rollover Week + 0x00, 0x00, 0x00, 0x00, // Fix Altitude + 0x00, 0x00, 0x00, 0x00, // Fix Height Error + 0x00, 0x00, 0x00, 0x00, // PDOP Maximum + 0x00, 0x00, 0x00, 0x00, // TDOP Maximum + 0x00, 0x00, 0x00, 0x00, // Position Accuracy Max + 0x00, 0x00, 0x00, 0x00, // Time Accuracy Max + 0x00, 0x00, 0x00, 0x00 // Static Hold Threshold +}; \ No newline at end of file diff --git a/src/gps/ubx.h b/src/gps/ubx.h new file mode 100644 index 0000000..68cca00 --- /dev/null +++ b/src/gps/ubx.h @@ -0,0 +1,478 @@ +const char *failMessage = "Unable to %s"; + +#define SEND_UBX_PACKET(TYPE, ID, DATA, ERRMSG, TIMEOUT) \ + msglen = makeUBXPacket(TYPE, ID, sizeof(DATA), DATA); \ + _serial_gps->write(UBXscratch, msglen); \ + if (getACK(TYPE, ID, TIMEOUT) != GNSS_RESPONSE_OK) { \ + LOG_WARN(failMessage, #ERRMSG); \ + } + +// Power Management + +uint8_t GPS::_message_PMREQ[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, // 4 bytes duration of request task (milliseconds) + 0x02, 0x00, 0x00, 0x00 // Bitfield, set backup = 1 +}; + +uint8_t GPS::_message_PMREQ_10[] PROGMEM = { + 0x00, // version (0 for this version) + 0x00, 0x00, 0x00, // Reserved 1 + 0x00, 0x00, 0x00, 0x00, // 4 bytes duration of request task (milliseconds) + 0x06, 0x00, 0x00, 0x00, // Bitfield, set backup =1 and force =1 + 0x08, 0x00, 0x00, 0x00 // wakeupSources Wake on uartrx +}; + +const uint8_t GPS::_message_CFG_RXM_PSM[] PROGMEM = { + 0x08, // Reserved + 0x01 // Power save mode +}; + +// only for Neo-6 +const uint8_t GPS::_message_CFG_RXM_ECO[] PROGMEM = { + 0x08, // Reserved + 0x04 // eco mode +}; + +const uint8_t GPS::_message_CFG_PM2[] PROGMEM = { + 0x01, // version + 0x00, // Reserved 1, set to 0x06 by u-Center + 0x00, // Reserved 2 + 0x00, // Reserved 1 + 0x00, 0x11, 0x03, 0x00, // flags-> cyclic mode, wait for normal fix ok, do not wake to update RTC, doNotEnterOff, + // LimitPeakCurrent + 0xE8, 0x03, 0x00, 0x00, // update period 1000 ms + 0x10, 0x27, 0x00, 0x00, // search period 10s + 0x00, 0x00, 0x00, 0x00, // Grid offset 0 + 0x01, 0x00, // onTime 1 second + 0x00, 0x00, // min search time 0 + 0x00, 0x00, // 0x2C, 0x01, // reserved 4 + 0x00, 0x00, // 0x00, 0x00, // reserved 5 + 0x00, 0x00, 0x00, 0x00, // 0x4F, 0xC1, 0x03, 0x00, // reserved 6 + 0x00, 0x00, 0x00, 0x00, // 0x87, 0x02, 0x00, 0x00, // reserved 7 + 0x00, // 0xFF, // reserved 8 + 0x00, // 0x00, // reserved 9 + 0x00, 0x00, // 0x00, 0x00, // reserved 10 + 0x00, 0x00, 0x00, 0x00 // 0x64, 0x40, 0x01, 0x00 // reserved 11 +}; + +// Constallation setup, none required for Neo-6 + +// For Neo-7 GPS & SBAS +const uint8_t GPS::_message_GNSS_7[] = { + 0x00, // msgVer (0 for this version) + 0x00, // numTrkChHw (max number of hardware channels, read only, so it's always 0) + 0xff, // numTrkChUse (max number of channels to use, 0xff = max available) + 0x02, // numConfigBlocks (number of GNSS systems), most modules support maximum 3 GNSS systems + // GNSS config format: gnssId, resTrkCh, maxTrkCh, reserved1, flags + 0x00, 0x08, 0x10, 0x00, 0x01, 0x00, 0x00, 0x01, // GPS + 0x01, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x01 // SBAS +}; + +// It's not critical if the module doesn't acknowledge this configuration. +// The module should operate adequately with its factory or previously saved settings. +// It appears that there is a firmware bug in some GPS modules: When an attempt is made +// to overwrite a saved state with identical values, no ACK/NAK is received, contrary to +// what is specified in the Ublox documentation. +// There is also a possibility that the module may be GPS-only. + +// For M8 GPS, GLONASS, Galileo, SBAS, QZSS +const uint8_t GPS::_message_GNSS_8[] = { + 0x00, // msgVer (0 for this version) + 0x00, // numTrkChHw (max number of hardware channels, read only, so it's always 0) + 0xff, // numTrkChUse (max number of channels to use, 0xff = max available) + 0x05, // numConfigBlocks (number of GNSS systems) + // GNSS config format: gnssId, resTrkCh, maxTrkCh, reserved1, flags + 0x00, 0x08, 0x10, 0x00, 0x01, 0x00, 0x01, 0x01, // GPS + 0x01, 0x01, 0x03, 0x00, 0x01, 0x00, 0x01, 0x01, // SBAS + 0x02, 0x04, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, // Galileo + 0x05, 0x00, 0x03, 0x00, 0x01, 0x00, 0x01, 0x01, // QZSS + 0x06, 0x08, 0x0E, 0x00, 0x01, 0x00, 0x01, 0x01 // GLONASS +}; +/* +// For M8 GPS, GLONASS, BeiDou, SBAS, QZSS +const uint8_t GPS::_message_GNSS_8_B[] = { + 0x00, // msgVer (0 for this version) + 0x00, // numTrkChHw (max number of hardware channels, read only, so it's always 0) + 0xff, // numTrkChUse (max number of channels to use, 0xff = max available) read only for protocol >23 + 0x05, // numConfigBlocks (number of GNSS systems) + // GNSS config format: gnssId, resTrkCh, maxTrkCh, reserved1, flags + 0x00, 0x08, 0x10, 0x00, 0x01, 0x00, 0x01, 0x01, // GPS + 0x01, 0x01, 0x03, 0x00, 0x01, 0x00, 0x01, 0x01, // SBAS + 0x03, 0x08, 0x10, 0x00, 0x01, 0x00, 0x01, 0x01, // BeiDou + 0x05, 0x00, 0x03, 0x00, 0x01, 0x00, 0x01, 0x01, // QZSS + 0x06, 0x08, 0x0E, 0x00, 0x01, 0x00, 0x01, 0x01 // GLONASS +}; +*/ + +// For M8 we want to enable NMEA version 4.10 messages to allow for Galileo and or BeiDou +const uint8_t GPS::_message_NMEA[]{ + 0x00, // filter flags + 0x41, // NMEA Version + 0x00, // Max number of SVs to report per TaklerId + 0x02, // flags + 0x00, 0x00, 0x00, 0x00, // gnssToFilter + 0x00, // svNumbering + 0x00, // mainTalkerId + 0x00, // gsvTalkerId + 0x01, // Message version + 0x00, 0x00, // bdsTalkerId 2 chars 0=default + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // Reserved +}; +// Enable jamming/interference monitor + +// For Neo-6, Max-7 and Neo-7 +const uint8_t GPS::_message_JAM_6_7[] = { + 0xf3, 0xac, 0x62, 0xad, // config1 bbThreshold = 3, cwThreshold = 15, enable = 1, reserved bits 0x16B156 + 0x1e, 0x03, 0x00, 0x00 // config2 antennaSetting Unknown = 0, reserved 3, = 0x00,0x00, reserved 2 = 0x31E +}; + +// For M8 +const uint8_t GPS::_message_JAM_8[] = { + 0xf3, 0xac, 0x62, 0xad, // config1 bbThreshold = 3, cwThreshold = 15, enable1 = 1, reserved bits 0x16B156 + 0x1e, 0x43, 0x00, 0x00 // config2 antennaSetting Unknown = 0, enable2 = 1, generalBits = 0x31E +}; + +// Configure navigation engine expert settings: +// there are many variations of what were Reserved fields for the Neo-6 in later versions +// ToDo: check UBX-MON-VER for module type and protocol version + +// For the Neo-6 +const uint8_t GPS::_message_NAVX5[] = { + 0x00, 0x00, // msgVer (0 for this version) + 0x4c, 0x66, // mask1 + 0x00, 0x00, 0x00, 0x00, // Reserved 0 + 0x00, // Reserved 1 + 0x00, // Reserved 2 + 0x03, // minSVs (Minimum number of satellites for navigation) = 3 + 0x10, // maxSVs (Maximum number of satellites for navigation) = 16 + 0x06, // minCNO (Minimum satellite signal level for navigation) = 6 dBHz + 0x00, // Reserved 5 + 0x00, // iniFix3D (Initial fix must be 3D) (0 = false 1 = true) + 0x00, // Reserved 6 + 0x00, // Reserved 7 + 0x00, // Reserved 8 + 0x00, 0x00, // wknRollover 0 = firmware default + 0x00, 0x00, 0x00, 0x00, // Reserved 9 + 0x00, // Reserved 10 + 0x00, // Reserved 11 + 0x00, // usePPP (Precice Point Positioning) (0 = false, 1 = true) + 0x01, // useAOP (AssistNow Autonomous configuration) = 1 (enabled) + 0x00, // Reserved 12 + 0x00, // Reserved 13 + 0x00, 0x00, // aopOrbMaxErr = 0 to reset to firmware default + 0x00, // Reserved 14 + 0x00, // Reserved 15 + 0x00, 0x00, // Reserved 3 + 0x00, 0x00, 0x00, 0x00 // Reserved 4 +}; +// For the M8 +const uint8_t GPS::_message_NAVX5_8[] = { + 0x02, 0x00, // msgVer (2 for this version) + 0x4c, 0x66, // mask1 + 0x00, 0x00, 0x00, 0x00, // mask2 + 0x00, 0x00, // Reserved 1 + 0x03, // minSVs (Minimum number of satellites for navigation) = 3 + 0x10, // maxSVs (Maximum number of satellites for navigation) = 16 + 0x06, // minCNO (Minimum satellite signal level for navigation) = 6 dBHz + 0x00, // Reserved 2 + 0x00, // iniFix3D (Initial fix must be 3D) (0 = false 1 = true) + 0x00, 0x00, // Reserved 3 + 0x00, // ackAiding + 0x00, 0x00, // wknRollover 0 = firmware default + 0x00, // sigAttenCompMode + 0x00, // Reserved 4 + 0x00, 0x00, // Reserved 5 + 0x00, 0x00, // Reserved 6 + 0x00, // usePPP (Precice Point Positioning) (0 = false, 1 = true) + 0x01, // aopCfg (AssistNow Autonomous configuration) = 1 (enabled) + 0x00, 0x00, // Reserved 7 + 0x00, 0x00, // aopOrbMaxErr = 0 to reset to firmware default + 0x00, 0x00, 0x00, 0x00, // Reserved 8 + 0x00, 0x00, 0x00, // Reserved 9 + 0x00 // useAdr +}; + +// Set GPS update rate to 1Hz +// Lowering the update rate helps to save power. +// Additionally, for some new modules like the M9/M10, an update rate lower than 5Hz +// is recommended to avoid a known issue with satellites disappearing. +// The module defaults for M8, M9, M10 are the same as we use here so no update is necessary +const uint8_t GPS::_message_1HZ[] = { + 0xE8, 0x03, // Measurement Rate (1000ms for 1Hz) + 0x01, 0x00, // Navigation rate, always 1 in GPS mode + 0x01, 0x00 // Time reference +}; + +// Disable GLL. GLL - Geographic position (latitude and longitude), which provides the current geographical +// coordinates. +const uint8_t GPS::_message_GLL[] = { + 0xF0, 0x01, // NMEA ID for GLL + 0x00, // Rate for DDC + 0x00, // Rate for UART1 + 0x00, // Rate for UART2 + 0x00, // Rate for USB + 0x00, // Rate for SPI + 0x00 // Reserved +}; + +// Disable GSA. GSA - GPS DOP and active satellites, used for detailing the satellites used in the positioning and +// the DOP (Dilution of Precision) +const uint8_t GPS::_message_GSA[] = { + 0xF0, 0x02, // NMEA ID for GSA + 0x00, // Rate for DDC + 0x00, // Rate for UART1 + 0x00, // Rate for UART2 + 0x00, // Rate for USB usefull for native linux + 0x00, // Rate for SPI + 0x00 // Reserved +}; + +// Disable GSV. GSV - Satellites in view, details the number and location of satellites in view. +const uint8_t GPS::_message_GSV[] = { + 0xF0, 0x03, // NMEA ID for GSV + 0x00, // Rate for DDC + 0x00, // Rate for UART1 + 0x00, // Rate for UART2 + 0x00, // Rate for USB + 0x00, // Rate for SPI + 0x00 // Reserved +}; + +// Disable VTG. VTG - Track made good and ground speed, which provides course and speed information relative to +// the ground. +const uint8_t GPS::_message_VTG[] = { + 0xF0, 0x05, // NMEA ID for VTG + 0x00, // Rate for DDC + 0x00, // Rate for UART1 + 0x00, // Rate for UART2 + 0x00, // Rate for USB + 0x00, // Rate for SPI + 0x00 // Reserved +}; + +// Enable RMC. RMC - Recommended Minimum data, the essential gps pvt (position, velocity, time) data. +const uint8_t GPS::_message_RMC[] = { + 0xF0, 0x04, // NMEA ID for RMC + 0x00, // Rate for DDC + 0x01, // Rate for UART1 + 0x00, // Rate for UART2 + 0x01, // Rate for USB usefull for native linux + 0x00, // Rate for SPI + 0x00 // Reserved +}; + +// Enable GGA. GGA - Global Positioning System Fix Data, which provides 3D location and accuracy data. +const uint8_t GPS::_message_GGA[] = { + 0xF0, 0x00, // NMEA ID for GGA + 0x00, // Rate for DDC + 0x01, // Rate for UART1 + 0x00, // Rate for UART2 + 0x01, // Rate for USB, usefull for native linux + 0x00, // Rate for SPI + 0x00 // Reserved +}; + +// Disable UBX-AID-ALPSRV as it may confuse TinyGPS. The Neo-6 seems to send this message +// whether the AID Autonomous is enabled or not +const uint8_t GPS::_message_AID[] = { + 0x0B, 0x32, // NMEA ID for UBX-AID-ALPSRV + 0x00, // Rate for DDC + 0x00, // Rate for UART1 + 0x00, // Rate for UART2 + 0x00, // Rate for USB + 0x00, // Rate for SPI + 0x00 // Reserved +}; + +// Turn off TEXT INFO Messages for all but M10 series + +// B5 62 06 02 0A 00 01 00 00 00 03 03 00 03 03 00 1F 20 +const uint8_t GPS::_message_DISABLE_TXT_INFO[] = { + 0x01, // Protocol ID for NMEA + 0x00, 0x00, 0x00, // Reserved + 0x03, // I2C + 0x03, // I/O Port 1 + 0x00, // I/O Port 2 + 0x03, // USB + 0x03, // SPI + 0x00 // Reserved +}; + +// The Power Management configuration allows the GPS module to operate in different power modes for optimized +// power consumption. The modes supported are: 0x00 = Full power: The module operates at full power with no power +// saving. 0x01 = Balanced: The module dynamically adjusts the tracking behavior to balance power consumption. +// 0x02 = Interval: The module operates in a periodic mode, cycling between tracking and power saving states. +// 0x03 = Aggressive with 1 Hz: The module operates in a power saving mode with a 1 Hz update rate. +// 0x04 = Aggressive with 2 Hz: The module operates in a power saving mode with a 2 Hz update rate. +// 0x05 = Aggressive with 4 Hz: The module operates in a power saving mode with a 4 Hz update rate. +// The 'period' field specifies the position update and search period. It is only valid when the powerSetupValue +// is set to Interval; otherwise, it must be set to '0'. The 'onTime' field specifies the duration of the ON phase +// and must be smaller than the period. It is only valid when the powerSetupValue is set to Interval; otherwise, +// it must be set to '0'. +// This command applies to M8 products +const uint8_t GPS::_message_PMS[] = { + 0x00, // Version (0) + 0x03, // Power setup value 3 = Agresssive 1Hz + 0x00, 0x00, // period: not applicable, set to 0 + 0x00, 0x00, // onTime: not applicable, set to 0 + 0x00, 0x00 // reserved, generated by u-center +}; + +const uint8_t GPS::_message_SAVE[] = { + 0x00, 0x00, 0x00, 0x00, // clearMask: no sections cleared + 0xFF, 0xFF, 0x00, 0x00, // saveMask: save all sections + 0x00, 0x00, 0x00, 0x00, // loadMask: no sections loaded + 0x17 // deviceMask: BBR, Flash, EEPROM, and SPI Flash +}; + +const uint8_t GPS::_message_SAVE_10[] = { + 0x00, 0x00, 0x00, 0x00, // clearMask: no sections cleared + 0xFF, 0xFF, 0x00, 0x00, // saveMask: save all sections + 0x00, 0x00, 0x00, 0x00, // loadMask: no sections loaded + 0x01 // deviceMask: only save to BBR +}; + +// 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 + +// VALSET Commands for M10 +// Please refer to the M10 Protocol Specification: +// https://content.u-blox.com/sites/default/files/u-blox-M10-SPG-5.10_InterfaceDescription_UBX-21035062.pdf +// Where the VALSET/VALGET/VALDEL commands are described in detail. +// and: +// https://content.u-blox.com/sites/default/files/u-blox-M10-ROM-5.10_ReleaseNotes_UBX-22001426.pdf +// for interesting insights. +// +// Integration manual: +// https://content.u-blox.com/sites/default/files/documents/SAM-M10Q_IntegrationManual_UBX-22020019.pdf +// has details on low-power modes + +/* +OPERATEMODE E1 2 (0 | 1 | 2) +POSUPDATEPERIOD U4 5 +ACQPERIOD U4 10 +GRIDOFFSET U4 0 +ONTIME U2 1 +MINACQTIME U1 0 +MAXACQTIME U1 0 +DONOTENTEROFF L 1 +WAITTIMEFIX L 1 +UPDATEEPH L 1 +EXTINTWAKE L 0 no ext ints +EXTINTBACKUP L 0 no ext ints +EXTINTINACTIVE L 0 no ext ints +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 +// 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 +// 10 01 8c 03 +*/ +const uint8_t GPS::_message_VALSET_PM_RAM[] = {0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xd0, 0x20, 0x02, 0x02, 0x00, 0xd0, 0x40, + 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0xd0, 0x30, 0x01, 0x00, 0x08, 0x00, 0xd0, + 0x10, 0x01, 0x09, 0x00, 0xd0, 0x10, 0x01, 0x10, 0x00, 0xd0, 0x10, 0x01}; +const uint8_t GPS::_message_VALSET_PM_BBR[] = {0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0xd0, 0x20, 0x02, 0x02, 0x00, 0xd0, 0x40, + 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0xd0, 0x30, 0x01, 0x00, 0x08, 0x00, 0xd0, + 0x10, 0x01, 0x09, 0x00, 0xd0, 0x10, 0x01, 0x10, 0x00, 0xd0, 0x10, 0x01}; + +/* +CFG-ITFM replaced by 5 valset messages which can be combined into one for RAM and one for BBR + +20410001 bbthreshold U1 3 +20410002 cwthreshold U1 15 +1041000d enable L 0 -> 1 +20410010 ant E1 0 +10410013 enable aux L 0 -> 1 + + +b5 62 06 8a 0e 00 00 01 00 00 0d 00 41 10 01 13 00 41 10 01 63 c6 +*/ +const uint8_t GPS::_message_VALSET_ITFM_RAM[] = {0x00, 0x01, 0x00, 0x00, 0x0d, 0x00, 0x41, + 0x10, 0x01, 0x13, 0x00, 0x41, 0x10, 0x01}; +const uint8_t GPS::_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 + +// 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 + +const uint8_t GPS::_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, 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}; + +const uint8_t GPS::_message_VALSET_DISABLE_NMEA_BBR[] = {0x00, 0x02, 0x00, 0x00, 0xca, 0x00, 0x91, 0x20, 0x00, 0xc5, + 0x00, 0x91, 0x20, 0x00, 0xb1, 0x00, 0x91, 0x20, 0x00}; + +// Turn off text info messages: +// Ram layer config message: +// b5 62 06 8a 09 00 00 01 00 00 07 00 92 20 06 59 50 + +// BBR layer config message: +// b5 62 06 8a 09 00 00 02 00 00 07 00 92 20 06 5a 58 + +// Turn NMEA GGA, RMC messages on: +// Layer config messages: +// RAM: +// b5 62 06 8a 0e 00 00 01 00 00 bb 00 91 20 01 ac 00 91 20 01 6a 8f +// BBR: +// b5 62 06 8a 0e 00 00 02 00 00 bb 00 91 20 01 ac 00 91 20 01 6b 9c +// FLASH: +// b5 62 06 8a 0e 00 00 04 00 00 bb 00 91 20 01 ac 00 91 20 01 6d b6 +// Doing this for the FLASH layer isn't really required since we save the config to flash later + +const uint8_t GPS::_message_VALSET_DISABLE_TXT_INFO_RAM[] = {0x00, 0x01, 0x00, 0x00, 0x07, 0x00, 0x92, 0x20, 0x03}; +const uint8_t GPS::_message_VALSET_DISABLE_TXT_INFO_BBR[] = {0x00, 0x02, 0x00, 0x00, 0x07, 0x00, 0x92, 0x20, 0x03}; + +const uint8_t GPS::_message_VALSET_ENABLE_NMEA_RAM[] = {0x00, 0x01, 0x00, 0x00, 0xbb, 0x00, 0x91, + 0x20, 0x01, 0xac, 0x00, 0x91, 0x20, 0x01}; +const uint8_t GPS::_message_VALSET_ENABLE_NMEA_BBR[] = {0x00, 0x02, 0x00, 0x00, 0xbb, 0x00, 0x91, + 0x20, 0x01, 0xac, 0x00, 0x91, 0x20, 0x01}; +const uint8_t GPS::_message_VALSET_DISABLE_SBAS_RAM[] = {0x00, 0x01, 0x00, 0x00, 0x20, 0x00, 0x31, + 0x10, 0x00, 0x05, 0x00, 0x31, 0x10, 0x00}; +const uint8_t GPS::_message_VALSET_DISABLE_SBAS_BBR[] = {0x00, 0x02, 0x00, 0x00, 0x20, 0x00, 0x31, + 0x10, 0x00, 0x05, 0x00, 0x31, 0x10, 0x00}; + +/* +Operational issues with the M10: + +PowerSave doesn't work with SBAS, seems like you can have SBAS enabled, but it will never lock +onto the SBAS sats. +PowerSave doesn't work with BDS B1C, u-blox says use B1l instead. +BDS B1l cannot be enabled with BDS B1C or GLONASS L1OF, so GLONASS will work with B1C, but not B1l +So no powersave with GLONASS and BDS B1l enabled. +So disable GLONASS and use BDS B1l, which is part of the default M10 config. + +GNSS configuration: + +Default GNSS configuration is: GPS, Galileo, BDS B1l, with QZSS and SBAS enabled. +The PMREQ puts the receiver to sleep and wakeup re-acquires really fast and seems to not need +the PM config. Lets try without it. +PMREQ sort of works with SBAS, but the awake time is too short to re-acquire any SBAS sats. +The defination of "Got Fix" doesn't seem to include SBAS. Much more too this... +Even if it was, it can take minutes (up to 12.5), +even under good sat visability conditions to re-acquire the SBAS data. + +Another effect fo the quick transition to sleep is that no other sats will be acquired so the +sat count will tend to remain at what the initial fix was. +*/ + +// GNSS disable SBAS as recommended by u-blox if using GNSS defaults and power save mode +/* +Ram layer config message: +b5 62 06 8a 0e 00 00 01 00 00 20 00 31 10 00 05 00 31 10 00 46 87 + +BBR layer config message: +b5 62 06 8a 0e 00 00 02 00 00 20 00 31 10 00 05 00 31 10 00 47 94 +*/ \ No newline at end of file diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp new file mode 100644 index 0000000..1d3b2f6 --- /dev/null +++ b/src/graphics/EInkDisplay2.cpp @@ -0,0 +1,204 @@ +#include "configuration.h" + +#ifdef USE_EINK +#include "EInkDisplay2.h" +#include "SPILock.h" +#include "main.h" +#include + +/* + The macros EINK_DISPLAY_MODEL, EINK_WIDTH, and EINK_HEIGHT are defined as build_flags in a variant's platformio.ini + Previously, these macros were defined at the top of this file. + + For archival reasons, note that the following configurations had also been tested during this period: + * ifdef RAK4631 + - 4.2 inch + EINK_DISPLAY_MODEL: GxEPD2_420_M01 + EINK_WIDTH: 300 + EINK_WIDTH: 400 + + - 2.9 inch + EINK_DISPLAY_MODEL: GxEPD2_290_T5D + EINK_WIDTH: 296 + EINK_HEIGHT: 128 + + - 1.54 inch + EINK_DISPLAY_MODEL: GxEPD2_154_M09 + EINK_WIDTH: 200 + EINK_HEIGHT: 200 +*/ + +// 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; + + // 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); +} + +/** + * 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); + + uint32_t now = millis(); + uint32_t sinceLast = now - lastDrawMsec; + + 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; + for (uint32_t y = 0; y < displayHeight; y++) { + for (uint32_t x = 0; x < displayWidth; x++) { + // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficient + auto b = buffer[x + (y / 8) * displayWidth]; + auto isset = b & (1 << (y & 7)); + + // Handle flip here, rather than with setRotation(), + // Avoids issues when display width is not a multiple of 8 + 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); + } + } + + // Trigger the refresh in GxEPD2 + LOG_DEBUG("Updating E-Paper"); + adafruitDisplay->nextPage(); + + // End the update process + endUpdate(); + + 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(); +} + +// 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) + + 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::setDetected(uint8_t detected) +{ + (void)detected; +} + +// Connect to the display - variant specific +bool EInkDisplay::connect() +{ + LOG_INFO("Doing EInk init"); + +#ifdef PIN_EINK_EN + // backlight power, HIGH is backlight on, LOW is off + pinMode(PIN_EINK_EN, OUTPUT); + digitalWrite(PIN_EINK_EN, LOW); +#endif + +#if defined(TTGO_T_ECHO) + { + 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_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213) || \ + defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_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); + } +#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) + 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); + } +#endif + + return true; +} + +#endif diff --git a/src/graphics/EInkDisplay2.h b/src/graphics/EInkDisplay2.h new file mode 100644 index 0000000..af63115 --- /dev/null +++ b/src/graphics/EInkDisplay2.h @@ -0,0 +1,80 @@ +#pragma once + +#ifdef USE_EINK + +#include "GxEPD2_BW.h" +#include + +/** + * An adapter class that allows using the GxEPD2 library as if it was an OLEDDisplay implementation. + * + * Note: EInkDynamicDisplay derives from this class. + * + * Remaining TODO: + * optimize display() to only draw changed pixels (see other OLED subclasses for examples) + * implement displayOn/displayOff to turn off the TFT device (and backlight) + * Use the fast NRF52 SPI API rather than the slow standard arduino version + * + * 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; + + 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; + + /** + * 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(); + + /** + * 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; } + + // Send a command to the display (low level function) + virtual void sendCommand(uint8_t com) override; + + // Connect to the display + virtual bool connect() override; + + // AdafruitGFX display object - instantiated in connect(), variant specific + GxEPD2_BW *adafruitDisplay = NULL; + + // 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) + SPIClass *hspi = NULL; +#endif + + private: + // FIXME quick hack to limit drawing to a very slow rate + uint32_t lastDrawMsec = 0; +}; + +#endif \ No newline at end of file diff --git a/src/graphics/EInkDynamicDisplay.cpp b/src/graphics/EInkDynamicDisplay.cpp new file mode 100644 index 0000000..ac5755b --- /dev/null +++ b/src/graphics/EInkDynamicDisplay.cpp @@ -0,0 +1,556 @@ +#include "Throttle.h" +#include "configuration.h" + +#if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) +#include "EInkDynamicDisplay.h" + +// 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 +#ifdef EINK_LIMIT_GHOSTING_PX + dirtyPixels = new uint8_t[EInkDisplay::displayBufferSize](); // Init with zeros +#endif +} + +// Destructor +EInkDynamicDisplay::~EInkDynamicDisplay() +{ + // If we were tracking ghost pixels, free the memory +#ifdef EINK_LIMIT_GHOSTING_PX + delete[] dirtyPixels; +#endif +} + +// Screen requests a BACKGROUND frame +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 +} + +// 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); +} + +// GxEPD2 code to set fast refresh +void EInkDynamicDisplay::configForFastRefresh() +{ + // Variant-specific code can go here +#if defined(PRIVATE_HW) +#else + // 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 +#if defined(PRIVATE_HW) +#else + // 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; + } + + // Change from FAST back to FULL + else if (currentConfig == FAST && refresh == FULL) { + configForFullRefresh(); + currentConfig = FULL; + } +} + +// Update fastRefreshCount +void EInkDynamicDisplay::adjustRefreshCounters() +{ + if (refresh == FAST) + fastRefreshCount++; + + 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 + + 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 +#ifdef HAS_EINK_ASYNCFULL + 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 (dropping 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. + + // 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(); + } +#endif +} + +// Assess situation, pick a refresh type +bool EInkDynamicDisplay::determineMode() +{ + checkInitialized(); + checkForPromotion(); +#if defined(HAS_EINK_ASYNCFULL) + checkBusyAsyncRefresh(); +#endif + checkRateLimiting(); + + // If too soon for a new frame, or display busy, abort early + if (refresh == SKIPPED) + return false; // No refresh + + // -- 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 + + // Once mode determined, any remaining checks will bypass + checkCosmetic(); + checkDemandingFast(); + checkFrameMatchesPrevious(); + checkConsecutiveFastRefreshes(); +#ifdef EINK_LIMIT_GHOSTING_PX + checkExcessiveGhosting(); +#endif + checkFastRequested(); + + if (refresh == UNSPECIFIED) + LOG_WARN("There was a flaw in the determineMode() logic."); + + // -- Decision has been reached -- + applyRefreshMode(); + adjustRefreshCounters(); + +#ifdef EINK_LIMIT_GHOSTING_PX + // 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 +} + +// Is this the very first frame? +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(); + + 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); + } +} + +// 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 + + 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; + + // Skip update: too soon for BACKGROUND + if (frameFlags == BACKGROUND) { + if (Throttle::isWithinTimespanMs(previousRunMs, EINK_LIMIT_RATE_BACKGROUND_SEC * 1000)) { + refresh = SKIPPED; + reason = EXCEEDED_RATELIMIT_FULL; + 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, EINK_LIMIT_RATE_RESPONSIVE_SEC * 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; + + // 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; + + // 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; + + // 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; + } +#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); +} + +// 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; + + // 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; + + 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); +#else + // 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); + } +} + +// Reset the timer used for rate-limiting +void EInkDynamicDisplay::resetRateLimiting() +{ + previousRunMs = millis(); +} + +// Generate a hash of this frame, to compare against previous update +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; + } +} + +// Store the results of determineMode() for future use, and reset for next call +void EInkDynamicDisplay::storeAndReset() +{ + previousFrameFlags = frameFlags; + previousRefresh = refresh; + previousReason = reason; + + // Only store image hash if the display will update + if (refresh != SKIPPED) { + previousImageHash = imageHash; + } + + 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; + + // 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++) { + + 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++; + + // 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); +} + +// 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; + + 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); + } +} + +// 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); +} +#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; + } +} + +#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; + + LOG_DEBUG("Joining an async refresh in progress"); + + // 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"); + + // 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; + + // 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"); + + // 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; + + // 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() + 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(); + + // 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 + +#endif // USE_EINK_DYNAMICDISPLAY \ No newline at end of file diff --git a/src/graphics/EInkDynamicDisplay.h b/src/graphics/EInkDynamicDisplay.h new file mode 100644 index 0000000..9e131dc --- /dev/null +++ b/src/graphics/EInkDynamicDisplay.h @@ -0,0 +1,149 @@ +#pragma once + +#include "configuration.h" + +#if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) + +#include "EInkDisplay2.h" +#include "GxEPD2_BW.h" +#include "concurrency/NotifiedWorkerThread.h" + +/* + Derives from the EInkDisplay adapter class. + Accepts suggestions from Screen class about frame type. + Determines which refresh type is most suitable. + (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(); + + // 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 + }; + 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. + + 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; + + 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? + + 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 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 + + // 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 +#endif + + // 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 + + 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 + + protected: + void pollAsyncRefresh() {} // Dummy method. In theory, not reachable +#endif +}; + +// Hide the ugly casts used in Screen.cpp +#define EINK_ADD_FRAMEFLAG(display, flag) static_cast(display)->addFrameFlag(EInkDynamicDisplay::flag) +#define EINK_JOIN_ASYNCREFRESH(display) static_cast(display)->joinAsyncRefresh() + +#else // !USE_EINK_DYNAMICDISPLAY +// Dummy-macro, removes the need for include guards +#define EINK_ADD_FRAMEFLAG(display, flag) +#define EINK_JOIN_ASYNCREFRESH(display) +#endif \ No newline at end of file diff --git a/src/graphics/NeoPixel.h b/src/graphics/NeoPixel.h new file mode 100644 index 0000000..dde7436 --- /dev/null +++ b/src/graphics/NeoPixel.h @@ -0,0 +1,4 @@ +#ifdef HAS_NEOPIXEL +#include +extern Adafruit_NeoPixel pixels; +#endif \ No newline at end of file diff --git a/src/graphics/PointStruct.h b/src/graphics/PointStruct.h new file mode 100644 index 0000000..2187319 --- /dev/null +++ b/src/graphics/PointStruct.h @@ -0,0 +1,4 @@ +struct PointStruct { + int x; + int y; +}; \ No newline at end of file diff --git a/src/graphics/RAKled.h b/src/graphics/RAKled.h new file mode 100644 index 0000000..659ea9b --- /dev/null +++ b/src/graphics/RAKled.h @@ -0,0 +1,5 @@ +#ifdef HAS_NCP5623 +#include +extern NCP5623 rgb; + +#endif \ No newline at end of file diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp new file mode 100644 index 0000000..efa3ec7 --- /dev/null +++ b/src/graphics/Screen.cpp @@ -0,0 +1,2750 @@ +/* + +SSD1306 - Screen module + +Copyright (C) 2018 by Xose Pérez + + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ +#include "Screen.h" +#include "../userPrefs.h" +#include "PowerMon.h" +#include "Throttle.h" +#include "configuration.h" +#if HAS_SCREEN +#include + +#include "DisplayFormatters.h" +#if !MESHTASTIC_EXCLUDE_GPS +#include "GPS.h" +#endif +#include "MeshService.h" +#include "NodeDB.h" +#include "error.h" +#include "gps/GeoCoord.h" +#include "gps/RTC.h" +#include "graphics/ScreenFonts.h" +#include "graphics/images.h" +#include "input/ScanAndSelect.h" +#include "input/TouchScreenImpl1.h" +#include "main.h" +#include "mesh-pb-constants.h" +#include "mesh/Channels.h" +#include "mesh/generated/meshtastic/deviceonly.pb.h" +#include "meshUtils.h" +#include "modules/AdminModule.h" +#include "modules/ExternalNotificationModule.h" +#include "modules/TextMessageModule.h" +#include "modules/WaypointModule.h" +#include "sleep.h" +#include "target_specific.h" + +#if HAS_WIFI && !defined(ARCH_PORTDUINO) +#include "mesh/wifi/WiFiAPClient.h" +#endif + +#ifdef ARCH_ESP32 +#include "esp_task_wdt.h" +#include "modules/StoreForwardModule.h" +#endif + +#if ARCH_PORTDUINO +#include "modules/StoreForwardModule.h" +#include "platform/portduino/PortduinoGlue.h" +#endif + +using namespace meshtastic; /** @todo remove */ + +namespace graphics +{ + +// This means the *visible* area (sh1106 can address 132, but shows 128 for example) +#define IDLE_FRAMERATE 1 // in fps + +// DEBUG +#define NUM_EXTRA_FRAMES 3 // text message and debug frame +// if defined a pixel will blink to show redraws +// #define SHOW_REDRAWS + +// A text message frame + debug frame + all the node infos +FrameCallback *normalFrames; +static uint32_t targetFramerate = IDLE_FRAMERATE; + +uint32_t logo_timeout = 5000; // 4 seconds for EACH logo + +uint32_t hours_in_month = 730; + +// This image definition is here instead of images.h because it's modified dynamically by the drawBattery function +uint8_t imgBattery[16] = {0xFF, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0xE7, 0x3C}; + +// Threshold values for the GPS lock accuracy bar display +uint32_t dopThresholds[5] = {2000, 1000, 500, 200, 100}; + +// At some point, we're going to ask all of the modules if they would like to display a screen frame +// we'll need to hold onto pointers for the modules that can draw a frame. +std::vector moduleFrames; + +// Stores the last 4 of our hardware ID, to make finding the device for pairing easier +static char ourId[5]; + +// vector where symbols (string) are displayed in bottom corner of display. +std::vector functionSymbals; +// string displayed in bottom right corner of display. Created from elements in functionSymbals vector +std::string functionSymbalString = ""; + +#if HAS_GPS +// GeoCoord object for the screen +GeoCoord geoCoord; +#endif + +#ifdef SHOW_REDRAWS +static bool heartbeat = false; +#endif + +// Quick access to screen dimensions from static drawing functions +// DEPRECATED. To-do: move static functions inside Screen class +#define SCREEN_WIDTH display->getWidth() +#define SCREEN_HEIGHT display->getHeight() + +#include "graphics/ScreenFonts.h" +#include + +#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2) + +/// Check if the display can render a string (detect special chars; emoji) +static bool haveGlyphs(const char *str) +{ +#if defined(OLED_PL) || defined(OLED_UA) || defined(OLED_RU) + // 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; + } + } + + LOG_DEBUG("haveGlyphs=%d", have); + return have; +} + +/** + * Draw the icon with extra info printed around the corners + */ +static void 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 + 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); +#ifdef USERPREFS_SPLASH_TITLE + const char *title = USERPREFS_SPLASH_TITLE; +#else + const char *title = "meshtastic.org"; +#endif + 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), 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_LEFT); // Restore left align, just to be kind to any other unsuspecting code +} + +void Screen::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); +} + +// Used on boot when a certificate is being created +static void 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(); +#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 . . "); + } +} + +// Used when booting without a region set +static void drawWelcomeScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(64 + x, y, "//\\ E S H T /\\ S T / C"); + display->drawString(64 + x, y + FONT_HEIGHT_SMALL, getDeviceName()); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + if ((millis() / 10000) % 2) { + display->drawString(x, y + FONT_HEIGHT_SMALL * 2 - 3, "Set the region using the"); + display->drawString(x, y + FONT_HEIGHT_SMALL * 3 - 3, "Meshtastic Android, iOS,"); + display->drawString(x, y + FONT_HEIGHT_SMALL * 4 - 3, "Web or CLI clients."); + } else { + display->drawString(x, y + FONT_HEIGHT_SMALL * 2 - 3, "Visit meshtastic.org"); + display->drawString(x, y + FONT_HEIGHT_SMALL * 3 - 3, "for more information."); + display->drawString(x, y + FONT_HEIGHT_SMALL * 4 - 3, ""); + } + +#ifdef ARCH_ESP32 + yield(); + esp_task_wdt_reset(); +#endif +} + +// draw overlay in bottom right corner of screen to show when notifications are muted or modifier key is active +static void drawFunctionOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + // LOG_DEBUG("Drawing function overlay"); + if (functionSymbals.begin() != functionSymbals.end()) { + char buf[64]; + display->setFont(FONT_SMALL); + snprintf(buf, sizeof(buf), "%s", functionSymbalString.c_str()); + display->drawString(SCREEN_WIDTH - display->getStringWidth(buf), SCREEN_HEIGHT - FONT_HEIGHT_SMALL, buf); + } +} + +#ifdef USE_EINK +/// Used on eink displays while in deep sleep +static void drawDeepSleepScreen(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); + + LOG_DEBUG("Drawing deep sleep screen"); + + // Display displayStr on the screen + drawIconScreen("Sleeping", display, state, x, y); +} + +/// Used on eink displays when screen updates are paused +static void drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + LOG_DEBUG("Drawing screensaver overlay"); + + EINK_ADD_FRAMEFLAG(display, COSMETIC); // Take the opportunity for a full-refresh + + // 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); // This bool is used to hide the idText box if we can't render the short name + constexpr uint16_t padding = 5; + constexpr uint8_t dividerGap = 1; + constexpr uint8_t imprecision = 5; // How far the box origins can drift from center. Combat burn-in. + + // Dimensions + const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true); // "true": handle utf8 chars + const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText)); + const uint16_t boxWidth = padding + (useId ? idTextWidth + padding + padding : 0) + pauseTextWidth + padding; + const uint16_t boxHeight = padding + FONT_HEIGHT_SMALL + padding; + + // Position + const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2) + random(-imprecision, imprecision + 1); + // const int16_t boxRight = boxLeft + boxWidth - 1; + const int16_t boxTop = (display->height() / 2) - (boxHeight / 2 + random(-imprecision, imprecision + 1)); + const int16_t boxBottom = boxTop + boxHeight - 1; + const int16_t idTextLeft = boxLeft + padding; + const int16_t idTextTop = boxTop + padding; + const int16_t pauseTextLeft = boxLeft + (useId ? padding + idTextWidth + padding : 0) + padding; + const int16_t pauseTextTop = boxTop + padding; + const int16_t dividerX = boxLeft + padding + idTextWidth + padding; + const int16_t dividerTop = boxTop + 1 + dividerGap; + const int16_t dividerBottom = boxBottom - 1 - dividerGap; + + // Draw: box + display->setColor(EINK_WHITE); + display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Clear a slightly oversized area for the box + 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: divider + if (useId) + display->drawLine(dividerX, dividerTop, dividerX, dividerBottom); +} +#endif + +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; + // LOG_DEBUG("Screen is not in transition. Frame: %d", module_frame); + } + // LOG_DEBUG("Drawing Module Frame %d", module_frame); + MeshModule &pi = *moduleFrames.at(module_frame); + pi.drawFrame(display, state, x, y); +} + +static void 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."); +} + +/// Draw the last text message we received +static void 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"); +} + +// Ignore messages originating from phone (from the current node 0x0) unless range test or store and forward module are enabled +static bool shouldDrawMessage(const meshtastic_MeshPacket *packet) +{ + return packet->from != 0 && !moduleConfig.store_forward.enabled; +} + +// Draw power bars or a charging indicator on an image of a battery, determined by battery charge voltage or percentage. +static void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer, const PowerStatus *powerStatus) +{ + static const uint8_t powerBar[3] = {0x81, 0xBD, 0xBD}; + static const uint8_t lightning[8] = {0xA1, 0xA1, 0xA5, 0xAD, 0xB5, 0xA5, 0x85, 0x85}; + // Clear the bar area on the battery image + for (int i = 1; i < 14; i++) { + imgBuffer[i] = 0x81; + } + // If charging, draw a charging indicator + if (powerStatus->getIsCharging()) { + memcpy(imgBuffer + 3, lightning, 8); + // If not charging, Draw power bars + } else { + for (int i = 0; i < 4; i++) { + if (powerStatus->getBatteryChargePercent() >= 25 * i) + memcpy(imgBuffer + 1 + (i * 3), powerBar, 3); + } + } + display->drawFastImage(x, y, 16, 8, imgBuffer); +} + +#ifdef T_WATCH_S3 + +void Screen::drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode, float scale) +{ + uint16_t segmentWidth = SEGMENT_WIDTH * scale; + uint16_t segmentHeight = SEGMENT_HEIGHT * scale; + + if (digitalMode) { + uint16_t radius = (segmentWidth + (segmentHeight * 2) + 4) / 2; + uint16_t centerX = (x + segmentHeight + 2) + (radius / 2); + uint16_t centerY = (y + segmentHeight + 2) + (radius / 2); + + display->drawCircle(centerX, centerY, radius); + display->drawCircle(centerX, centerY, radius + 1); + display->drawLine(centerX, centerY, centerX, centerY - radius + 3); + display->drawLine(centerX, centerY, centerX + radius - 3, centerY); + } else { + 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 = segmentOneX; + uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2; + + uint16_t segmentFourX = x; + uint16_t segmentFourY = y + segmentHeight + 2; + + drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); + drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); + drawHorizontalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); + drawVerticalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); + } +} + +// Draw a digital clock +void Screen::drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setTextAlignment(TEXT_ALIGN_LEFT); + + drawBattery(display, x, y + 7, imgBattery, powerStatus); + + if (powerStatus->getHasBattery()) { + String batteryPercent = String(powerStatus->getBatteryChargePercent()) + "%"; + + display->setFont(FONT_SMALL); + + display->drawString(x + 20, y + 2, batteryPercent); + } + + if (nimbleBluetooth && nimbleBluetooth->isConnected()) { + drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2); + } + + drawWatchFaceToggleButton(display, display->getWidth() - 36, display->getHeight() - 36, screen->digitalWatchFace, 1); + + display->setColor(OLEDDISPLAY_COLOR::WHITE); + + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone + if (rtc_sec > 0) { + long hms = rtc_sec % SEC_PER_DAY; + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + + int hour = hms / SEC_PER_HOUR; + int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN + + hour = hour > 12 ? hour - 12 : hour; + + if (hour == 0) { + hour = 12; + } + + // hours string + String hourString = String(hour); + + // minutes string + String minuteString = minute < 10 ? "0" + String(minute) : String(minute); + + String timeString = hourString + ":" + minuteString; + + // seconds string + String secondString = second < 10 ? "0" + String(second) : String(second); + + float scale = 1.5; + + uint16_t segmentWidth = SEGMENT_WIDTH * scale; + uint16_t segmentHeight = SEGMENT_HEIGHT * scale; + + // calculate hours:minutes string width + uint16_t timeStringWidth = timeString.length() * 5; + + for (uint8_t i = 0; i < timeString.length(); i++) { + String character = String(timeString[i]); + + if (character == ":") { + timeStringWidth += segmentHeight; + } else { + timeStringWidth += segmentWidth + (segmentHeight * 2) + 4; + } + } + + // calculate seconds string width + uint16_t secondStringWidth = (secondString.length() * 12) + 4; + + // sum these to get total string width + uint16_t totalWidth = timeStringWidth + secondStringWidth; + + uint16_t hourMinuteTextX = (display->getWidth() / 2) - (totalWidth / 2); + + uint16_t startingHourMinuteTextX = hourMinuteTextX; + + uint16_t hourMinuteTextY = (display->getHeight() / 2) - (((segmentWidth * 2) + (segmentHeight * 3) + 8) / 2); + + // iterate over characters in hours:minutes string and draw segmented characters + for (uint8_t i = 0; i < timeString.length(); i++) { + String character = String(timeString[i]); + + if (character == ":") { + drawSegmentedDisplayColon(display, hourMinuteTextX, hourMinuteTextY, scale); + + hourMinuteTextX += segmentHeight + 6; + } else { + drawSegmentedDisplayCharacter(display, hourMinuteTextX, hourMinuteTextY, character.toInt(), scale); + + hourMinuteTextX += segmentWidth + (segmentHeight * 2) + 4; + } + + hourMinuteTextX += 5; + } + + // draw seconds string + display->setFont(FONT_MEDIUM); + display->drawString(startingHourMinuteTextX + timeStringWidth + 4, + (display->getHeight() - hourMinuteTextY) - FONT_HEIGHT_MEDIUM + 6, secondString); + } +} + +void Screen::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 topAndBottomX = x + (4 * scale); + + uint16_t quarterCellHeight = cellHeight / 4; + + uint16_t topY = y + quarterCellHeight; + uint16_t bottomY = y + (quarterCellHeight * 3); + + display->fillRect(topAndBottomX, topY, segmentHeight, segmentHeight); + display->fillRect(topAndBottomX, bottomY, segmentHeight, segmentHeight); +} + +void Screen::drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale) +{ + // the numbers 0-9, each expressed as an array of seven boolean (0|1) values encoding the on/off state of + // segment {innerIndex + 1} + // e.g., to display the numeral '0', segments 1-6 are on, and segment 7 is off. + uint8_t numbers[10][7] = { + {1, 1, 1, 1, 1, 1, 0}, // 0 Display segment key + {0, 1, 1, 0, 0, 0, 0}, // 1 1 + {1, 1, 0, 1, 1, 0, 1}, // 2 ___ + {1, 1, 1, 1, 0, 0, 1}, // 3 6 | | 2 + {0, 1, 1, 0, 0, 1, 1}, // 4 |_7̲_| + {1, 0, 1, 1, 0, 1, 1}, // 5 5 | | 3 + {1, 0, 1, 1, 1, 1, 1}, // 6 |___| + {1, 1, 1, 0, 0, 1, 0}, // 7 + {1, 1, 1, 1, 1, 1, 1}, // 8 4 + {1, 1, 1, 1, 0, 1, 1}, // 9 + }; + + // the width and height of each segment's central rectangle: + // _____________________ + // ⋰| (only this part, |⋱ + // ⋰ | not including | ⋱ + // ⋱ | the triangles | ⋰ + // ⋱| on the ends) |⋰ + // ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ + + uint16_t segmentWidth = SEGMENT_WIDTH * scale; + uint16_t segmentHeight = SEGMENT_HEIGHT * scale; + + // segment x and y coordinates + 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; + + if (numbers[number][0]) { + drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); + } + + if (numbers[number][1]) { + drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); + } + + if (numbers[number][2]) { + drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); + } + + if (numbers[number][3]) { + drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); + } + + if (numbers[number][4]) { + drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight); + } + + if (numbers[number][5]) { + drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight); + } + + if (numbers[number][6]) { + drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight); + } +} + +void Screen::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 Screen::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 Screen::drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y) +{ + display->drawFastImage(x, y, 18, 14, bluetoothConnectedIcon); +} + +// Draw an analog clock +void Screen::drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setTextAlignment(TEXT_ALIGN_LEFT); + + drawBattery(display, x, y + 7, imgBattery, powerStatus); + + if (powerStatus->getHasBattery()) { + String batteryPercent = String(powerStatus->getBatteryChargePercent()) + "%"; + + display->setFont(FONT_SMALL); + + display->drawString(x + 20, y + 2, batteryPercent); + } + + if (nimbleBluetooth && nimbleBluetooth->isConnected()) { + drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2); + } + + drawWatchFaceToggleButton(display, display->getWidth() - 36, display->getHeight() - 36, screen->digitalWatchFace, 1); + + // clock face center coordinates + int16_t centerX = display->getWidth() / 2; + int16_t centerY = display->getHeight() / 2; + + // clock face radius + int16_t radius = (display->getWidth() / 2) * 0.8; + + // 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; + + // tick mark outer y coordinate; (first nested circle) + int16_t tickMarkOuterNoonY = secondHandNoonY; + + // seconds tick mark inner y coordinate; (second nested circle) + double secondsTickMarkInnerNoonY = (double)noonY + 8; + + // hours tick mark inner y coordinate; (third nested circle) + double hoursTickMarkInnerNoonY = (double)noonY + 16; + + // minute hand y coordinate + int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4; + + // hour string y coordinate + int16_t hourStringNoonY = minuteHandNoonY + 18; + + // hour hand radius and y coordinate + int16_t 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) { + long hms = rtc_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 minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN + + hour = hour > 12 ? hour - 12 : hour; + + int16_t degreesPerHour = 30; + int16_t degreesPerMinuteOrSecond = 6; + + double hourBaseAngle = hour * degreesPerHour; + double hourAngleOffset = ((double)minute / 60) * degreesPerHour; + double hourAngle = radians(hourBaseAngle + hourAngleOffset); + + 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; + + // draw hour number + display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); + } + + if (angle % degreesPerMinuteOrSecond == 0) { + double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX; + double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY; + + // 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); + + // draw second hand + display->drawLine(centerX, centerY, secondX, secondY); + } +} + +#endif + +// Get an absolute time from "seconds ago" info. Returns false if no valid timestamp possible +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; + return validCached; +} + +/// Draw the last text message we received +static void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // the max length of this buffer is much longer than we can possibly print + static char tempBuf[237]; + + const meshtastic_MeshPacket &mp = devicestate.rx_text_message; + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); + // LOG_DEBUG("drawing text message from 0x%x: %s", mp.from, + // mp.decoded.variant.data.decoded.bytes); + + // Demo for drawStringMaxWidth: + // with the third parameter you can define the width after which words will + // be wrapped. Currently only spaces and "-" are allowed for wrapping + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + 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); + } + + // For time delta + uint32_t seconds = sinceReceived(&mp); + uint32_t minutes = seconds / 60; + uint32_t hours = minutes / 60; + uint32_t days = hours / 24; + + // For timestamp + uint8_t timestampHours, timestampMinutes; + int32_t daysAgo; + bool useTimestamp = deltaToTimestamp(seconds, ×tampHours, ×tampMinutes, &daysAgo); + + // If bold, draw twice, shifting right by one pixel + for (uint8_t xOff = 0; xOff <= (config.display.heading_bold ? 1 : 0); xOff++) { + // Show a timestamp if received today, but longer than 15 minutes ago + if (useTimestamp && minutes >= 15 && daysAgo == 0) { + display->drawStringf(xOff + x, 0 + y, tempBuf, "At %02hu:%02hu from %s", timestampHours, timestampMinutes, + (node && node->has_user) ? node->user.short_name : "???"); + } + // Timestamp yesterday (if display is wide enough) + else if (useTimestamp && daysAgo == 1 && display->width() >= 200) { + display->drawStringf(xOff + x, 0 + y, tempBuf, "Yesterday %02hu:%02hu from %s", timestampHours, timestampMinutes, + (node && node->has_user) ? node->user.short_name : "???"); + } + // Otherwise, show a time delta + else { + display->drawStringf(xOff + x, 0 + y, tempBuf, "%s ago from %s", + screen->drawTimeDelta(days, hours, minutes, seconds).c_str(), + (node && node->has_user) ? node->user.short_name : "???"); + } + } + + display->setColor(WHITE); +#ifndef EXCLUDE_EMOJI + if (strcmp(reinterpret_cast(mp.decoded.payload.bytes), "\U0001F44D") == 0) { + display->drawXbm(x + (SCREEN_WIDTH - thumbs_width) / 2, + y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - thumbs_height) / 2 + 2 + 5, thumbs_width, thumbs_height, + thumbup); + } else if (strcmp(reinterpret_cast(mp.decoded.payload.bytes), "\U0001F44E") == 0) { + display->drawXbm(x + (SCREEN_WIDTH - thumbs_width) / 2, + y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - thumbs_height) / 2 + 2 + 5, thumbs_width, thumbs_height, + thumbdown); + } else if (strcmp(reinterpret_cast(mp.decoded.payload.bytes), "❓") == 0) { + display->drawXbm(x + (SCREEN_WIDTH - question_width) / 2, + y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - question_height) / 2 + 2 + 5, question_width, question_height, + question); + } else if (strcmp(reinterpret_cast(mp.decoded.payload.bytes), "‼️") == 0) { + display->drawXbm(x + (SCREEN_WIDTH - bang_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - bang_height) / 2 + 2 + 5, + bang_width, bang_height, bang); + } else if (strcmp(reinterpret_cast(mp.decoded.payload.bytes), "\U0001F4A9") == 0) { + display->drawXbm(x + (SCREEN_WIDTH - poo_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - poo_height) / 2 + 2 + 5, + poo_width, poo_height, poo); + } else if (strcmp(reinterpret_cast(mp.decoded.payload.bytes), "\xf0\x9f\xa4\xa3") == 0) { + display->drawXbm(x + (SCREEN_WIDTH - haha_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - haha_height) / 2 + 2 + 5, + haha_width, haha_height, haha); + } else if (strcmp(reinterpret_cast(mp.decoded.payload.bytes), "\U0001F44B") == 0) { + display->drawXbm(x + (SCREEN_WIDTH - wave_icon_width) / 2, + y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - wave_icon_height) / 2 + 2 + 5, wave_icon_width, + wave_icon_height, wave_icon); + } else if (strcmp(reinterpret_cast(mp.decoded.payload.bytes), "\U0001F920") == 0) { + display->drawXbm(x + (SCREEN_WIDTH - cowboy_width) / 2, + y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - cowboy_height) / 2 + 2 + 5, cowboy_width, cowboy_height, + cowboy); + } else if (strcmp(reinterpret_cast(mp.decoded.payload.bytes), "\U0001F42D") == 0) { + display->drawXbm(x + (SCREEN_WIDTH - deadmau5_width) / 2, + y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - deadmau5_height) / 2 + 2 + 5, deadmau5_width, deadmau5_height, + deadmau5); + } else if (strcmp(reinterpret_cast(mp.decoded.payload.bytes), "\xE2\x98\x80\xEF\xB8\x8F") == 0) { + display->drawXbm(x + (SCREEN_WIDTH - sun_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - sun_height) / 2 + 2 + 5, + sun_width, sun_height, sun); + } else if (strcmp(reinterpret_cast(mp.decoded.payload.bytes), "\u2614") == 0) { + display->drawXbm(x + (SCREEN_WIDTH - rain_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - rain_height) / 2 + 2 + 10, + rain_width, rain_height, rain); + } else if (strcmp(reinterpret_cast(mp.decoded.payload.bytes), "☁️") == 0) { + display->drawXbm(x + (SCREEN_WIDTH - cloud_width) / 2, + y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - cloud_height) / 2 + 2 + 5, cloud_width, cloud_height, cloud); + } else if (strcmp(reinterpret_cast(mp.decoded.payload.bytes), "🌫️") == 0) { + display->drawXbm(x + (SCREEN_WIDTH - fog_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - fog_height) / 2 + 2 + 5, + fog_width, fog_height, fog); + } else if (strcmp(reinterpret_cast(mp.decoded.payload.bytes), "\xf0\x9f\x98\x88") == 0) { + display->drawXbm(x + (SCREEN_WIDTH - devil_width) / 2, + y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - devil_height) / 2 + 2 + 5, devil_width, devil_height, devil); + } else if (strcmp(reinterpret_cast(mp.decoded.payload.bytes), "♥️") == 0) { + display->drawXbm(x + (SCREEN_WIDTH - heart_width) / 2, + y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - heart_height) / 2 + 2 + 5, heart_width, heart_height, heart); + } else { + snprintf(tempBuf, sizeof(tempBuf), "%s", mp.decoded.payload.bytes); + display->drawStringMaxWidth(0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(), tempBuf); + } +#else + snprintf(tempBuf, sizeof(tempBuf), "%s", mp.decoded.payload.bytes); + display->drawStringMaxWidth(0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(), tempBuf); +#endif +} + +/// Draw a series of fields in a column, wrapping to multiple columns if needed +void Screen::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); + + display->setColor(WHITE); + yo += FONT_HEIGHT_SMALL; + if (yo > SCREEN_HEIGHT - FONT_HEIGHT_SMALL) { + xo += SCREEN_WIDTH / 2; + yo = 0; + } + f++; + } +} + +// Draw nodes status +static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const NodeStatus *nodeStatus) +{ + char usersString[20]; + snprintf(usersString, sizeof(usersString), "%d/%d", nodeStatus->getNumOnline(), nodeStatus->getNumTotal()); +#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(DISPLAY_FORCE_SMALL_FONTS) + display->drawFastImage(x, y + 3, 8, 8, imgUser); +#else + display->drawFastImage(x, y, 8, 8, imgUser); +#endif + display->drawString(x + 10, y - 2, usersString); + if (config.display.heading_bold) + display->drawString(x + 11, y - 2, usersString); +} +#if HAS_GPS +// Draw GPS status summary +static void drawGPS(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus *gps) +{ + if (config.position.fixed_position) { + // GPS coordinates are currently fixed + display->drawString(x - 1, y - 2, "Fixed GPS"); + if (config.display.heading_bold) + display->drawString(x, y - 2, "Fixed GPS"); + return; + } + if (!gps->getIsConnected()) { + display->drawString(x, y - 2, "No GPS"); + if (config.display.heading_bold) + display->drawString(x + 1, y - 2, "No GPS"); + return; + } + display->drawFastImage(x, y, 6, 8, gps->getHasLock() ? imgPositionSolid : imgPositionEmpty); + if (!gps->getHasLock()) { + display->drawString(x + 8, y - 2, "No sats"); + if (config.display.heading_bold) + display->drawString(x + 9, y - 2, "No sats"); + return; + } else { + char satsString[3]; + uint8_t bar[2] = {0}; + + // Draw DOP signal bars + for (int i = 0; i < 5; i++) { + if (gps->getDOP() <= dopThresholds[i]) + bar[0] = ~((1 << (5 - i)) - 1); + else + bar[0] = 0b10000000; + // bar[1] = bar[0]; + display->drawFastImage(x + 9 + (i * 2), y, 2, 8, bar); + } + + // Draw satellite image + display->drawFastImage(x + 24, y, 8, 8, imgSatellite); + + // Draw the number of satellites + snprintf(satsString, sizeof(satsString), "%u", gps->getNumSatellites()); + display->drawString(x + 34, y - 2, satsString); + if (config.display.heading_bold) + display->drawString(x + 35, y - 2, satsString); + } +} + +// Draw status when GPS is disabled or not present +static void drawGPSpowerstat(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus *gps) +{ + String 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 = SCREEN_WIDTH - display->getStringWidth(displayLine); + } else { + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "GPS not present" + : "GPS is disabled"; + pos = (SCREEN_WIDTH - display->getStringWidth(displayLine)) / 2; + } + display->drawString(x + pos, y, displayLine); +} + +static void drawGPSAltitude(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus *gps) +{ + String displayLine = ""; + 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())); + displayLine = "Altitude: " + String(geoCoord.getAltitude()) + "m"; + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) + displayLine = "Altitude: " + String(geoCoord.getAltitude() * METERS_TO_FEET) + "ft"; + display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); + } +} + +// Draw GPS status coordinates +static void drawGPScoordinates(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus *gps) +{ + auto gpsFormat = config.display.gps_format; + String displayLine = ""; + + if (!gps->getIsConnected() && !config.position.fixed_position) { + displayLine = "No GPS present"; + 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 (gpsFormat != meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) { + char coordinateLine[22]; + if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees + snprintf(coordinateLine, sizeof(coordinateLine), "%f %f", geoCoord.getLatitude() * 1e-7, + geoCoord.getLongitude() * 1e-7); + } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator + snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %06u %07u", geoCoord.getUTMZone(), geoCoord.getUTMBand(), + geoCoord.getUTMEasting(), geoCoord.getUTMNorthing()); + } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System + snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %1c%1c %05u %05u", geoCoord.getMGRSZone(), + geoCoord.getMGRSBand(), geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k(), + geoCoord.getMGRSEasting(), geoCoord.getMGRSNorthing()); + } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OLC) { // Open Location Code + geoCoord.getOLCCode(coordinateLine); + } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference + if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') // OSGR is only valid around the UK region + snprintf(coordinateLine, sizeof(coordinateLine), "%s", "Out of Boundary"); + else + snprintf(coordinateLine, sizeof(coordinateLine), "%1c%1c %05u %05u", geoCoord.getOSGRE100k(), + geoCoord.getOSGRN100k(), geoCoord.getOSGREasting(), geoCoord.getOSGRNorthing()); + } + + // If fixed position, display text "Fixed GPS" alternating with the coordinates. + if (config.position.fixed_position) { + if ((millis() / 10000) % 2) { + display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(coordinateLine))) / 2, y, coordinateLine); + } else { + display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth("Fixed GPS"))) / 2, y, "Fixed GPS"); + } + } else { + display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(coordinateLine))) / 2, y, coordinateLine); + } + } else { + char latLine[22]; + char lonLine[22]; + snprintf(latLine, sizeof(latLine), "%2i° %2i' %2u\" %1c", geoCoord.getDMSLatDeg(), geoCoord.getDMSLatMin(), + geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP()); + snprintf(lonLine, sizeof(lonLine), "%3i° %2i' %2u\" %1c", geoCoord.getDMSLonDeg(), geoCoord.getDMSLonMin(), + geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP()); + display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(latLine))) / 2, y - FONT_HEIGHT_SMALL * 1, latLine); + display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(lonLine))) / 2, y, lonLine); + } + } +} +#endif +/** + * Given a recent lat/lon return a guess of the heading the user is walking on. + * + * 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; + + 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); + oldLat = lat; + oldLon = lon; + + return b; +} + +/// We will skip one node - the one for us, so we just blindly loop over all +/// nodes +static size_t nodeIndex; +static int8_t prevFrame = -1; + +// Draw the arrow pointing to a node's location +void Screen::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 = 1.0f; + Point leftArrow(tip.x - arrowOffsetX, tip.y - arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y - arrowOffsetY); + + 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); + } + /* Old arrow + display->drawLine(tip.x, tip.y, tail.x, tail.y); + display->drawLine(leftArrow.x, leftArrow.y, tip.x, tip.y); + display->drawLine(rightArrow.x, rightArrow.y, tip.x, tip.y); + display->drawLine(leftArrow.x, leftArrow.y, tail.x, tail.y); + display->drawLine(rightArrow.x, rightArrow.y, tail.x, tail.y); + */ +#ifdef USE_EINK + 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); +#endif + display->drawTriangle(tip.x, tip.y, leftArrow.x, leftArrow.y, tail.x, tail.y); +} + +// Get a string representation of the time passed since something happened +void Screen::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) < (hours_in_month * 6)) + snprintf(timeStr, maxLength, "%u hours ago", agoSecs / 60 / 60); + else + snprintf(timeStr, maxLength, "unknown age"); +} + +void Screen::drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading) +{ + // If north is supposed to be at the top of the compass we want rotation to be +0 + if (config.display.compass_north_top) + myHeading = -0; + /* N sign points currently not deleted*/ + Point N1(-0.04f, 0.65f), N2(0.04f, 0.65f); // N sign points (N1-N4) + Point N3(-0.04f, 0.55f), N4(0.04f, 0.55f); + Point NC1(0.00f, 0.50f); // north circle center point + Point *rosePoints[] = {&N1, &N2, &N3, &N4, &NC1}; + + uint16_t compassDiam = Screen::getCompassDiam(SCREEN_WIDTH, SCREEN_HEIGHT); + + for (int i = 0; i < 5; i++) { + // North on compass will be negative of heading + rosePoints[i]->rotate(-myHeading); + rosePoints[i]->scale(compassDiam); + rosePoints[i]->translate(compassX, compassY); + } + + /* changed the N sign to a small circle on the compass circle. + display->drawLine(N1.x, N1.y, N3.x, N3.y); + display->drawLine(N2.x, N2.y, N4.x, N4.y); + display->drawLine(N1.x, N1.y, N4.x, N4.y); + */ + display->drawCircle(NC1.x, NC1.y, 4); // North sign circle, 4px radius is sufficient for all displays. +} + +uint16_t Screen::getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) +{ + uint16_t diam = 0; + uint16_t offset = 0; + + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) + offset = FONT_HEIGHT_SMALL; + + // get the smaller of the 2 dimensions and subtract 20 + if (displayWidth > (displayHeight - offset)) { + diam = displayHeight - offset; + // if 2/3 of the other size would be smaller, use that + if (diam > (displayWidth * 2 / 3)) { + diam = displayWidth * 2 / 3; + } + } else { + diam = displayWidth; + if (diam > ((displayHeight - offset) * 2 / 3)) { + diam = (displayHeight - offset) * 2 / 3; + } + } + + return diam - 20; +}; + +static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // We only advance our nodeIndex if the frame # has changed - because + // drawNodeInfo will be called repeatedly while the frame is shown + if (state->currentFrame != prevFrame) { + prevFrame = state->currentFrame; + + nodeIndex = (nodeIndex + 1) % nodeDB->getNumMeshNodes(); + meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(nodeIndex); + if (n->num == nodeDB->getNodeNum()) { + // Don't show our node, just skip to next + nodeIndex = (nodeIndex + 1) % nodeDB->getNumMeshNodes(); + n = nodeDB->getMeshNodeByIndex(nodeIndex); + } + } + + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(nodeIndex); + + display->setFont(FONT_SMALL); + + // 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); + } + + const char *username = node->has_user ? node->user.long_name : "Unknown Name"; + + static char signalStr[20]; + + // section here to choose whether to display hops away rather than signal strength if more than 0 hops away. + if (node->hops_away > 0) { + snprintf(signalStr, sizeof(signalStr), "Hops Away: %d", node->hops_away); + } else { + snprintf(signalStr, sizeof(signalStr), "Signal: %d%%", clamp((int)((node->snr + 10) * 5), 0, 100)); + } + + static char lastStr[20]; + screen->getTimeAgoStr(sinceLastSeen(node), lastStr, sizeof(lastStr)); + + static char distStr[20]; + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + strncpy(distStr, "? mi", sizeof(distStr)); // might not have location data + } else { + strncpy(distStr, "? km", sizeof(distStr)); + } + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + const char *fields[] = {username, lastStr, signalStr, distStr, NULL}; + int16_t compassX = 0, compassY = 0; + uint16_t compassDiam = Screen::getCompassDiam(SCREEN_WIDTH, SCREEN_HEIGHT); + + // coordinates for the center of the compass/circle + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { + compassX = x + SCREEN_WIDTH - compassDiam / 2 - 5; + compassY = y + SCREEN_HEIGHT / 2; + } else { + compassX = x + SCREEN_WIDTH - compassDiam / 2 - 5; + compassY = y + FONT_HEIGHT_SMALL + (SCREEN_HEIGHT - FONT_HEIGHT_SMALL) / 2; + } + bool hasNodeHeading = false; + + if (ourNode && (hasValidPosition(ourNode) || screen->hasHeading())) { + const meshtastic_PositionLite &op = ourNode->position; + float myHeading; + if (screen->hasHeading()) + myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians + else + myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + screen->drawCompassNorth(display, compassX, compassY, myHeading); + + if (hasValidPosition(node)) { + // display direction toward node + hasNodeHeading = true; + const meshtastic_PositionLite &p = node->position; + float d = + GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + if (d < (2 * MILES_TO_FEET)) + snprintf(distStr, sizeof(distStr), "%.0f ft", d * METERS_TO_FEET); + else + snprintf(distStr, sizeof(distStr), "%.1f mi", d * METERS_TO_FEET / MILES_TO_FEET); + } else { + if (d < 2000) + snprintf(distStr, sizeof(distStr), "%.0f m", d); + else + snprintf(distStr, sizeof(distStr), "%.1f km", d / 1000); + } + + float bearingToOther = + GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.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 (!config.display.compass_north_top) + bearingToOther -= myHeading; + screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); + } + } + if (!hasNodeHeading) { + // direction to node is unknown so display question mark + // Debug info for gps lock errors + // LOG_DEBUG("ourNode %d, ourPos %d, theirPos %d", !!ourNode, ourNode && hasValidPosition(ourNode), + // hasValidPosition(node)); + display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?"); + } + display->drawCircle(compassX, compassY, compassDiam / 2); + + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + display->setColor(BLACK); + } + // Must be after distStr is populated + screen->drawColumns(display, x, y, fields); +} + +#if defined(ESP_PLATFORM) && defined(USE_ST7789) +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]; +#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); +#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); +#else + dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_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); +#elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || \ + defined(RAK14014) || defined(HX8357_CS) + 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 (settingsMap[displayPanel] != no_screen) { + LOG_DEBUG("Making 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; +#endif + + ui = new OLEDDisplayUi(dispdev); + cmdQueue.setReader(this); +} + +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() +{ +#ifdef USE_EINK + setOn(false, drawDeepSleepScreen); +#ifdef PIN_EINK_EN + digitalWrite(PIN_EINK_EN, LOW); // power off backlight +#endif +#else + // Without E-Ink display: + setOn(false); +#endif +} + +void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) +{ + if (!useDisplay) + return; + + if (on != screenOn) { + if (on) { + LOG_INFO("Turning on screen"); + powerMon->setState(meshtastic_PowerMon_State_Screen_On); +#ifdef T_WATCH_S3 + PMU->enablePowerOutput(XPOWERS_ALDO2); +#endif +#if !ARCH_PORTDUINO + dispdev->displayOn(); +#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); +#endif + + dispdev->displayOn(); +#ifdef USE_ST7789 + pinMode(VTFT_CTRL, OUTPUT); + digitalWrite(VTFT_CTRL, LOW); + ui->init(); +#ifdef ESP_PLATFORM + analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT); +#else + 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); +#ifdef USE_EINK + // eInkScreensaver parameter is usually NULL (default argument), default frame used instead + setScreensaverFrames(einkScreensaver); +#endif + LOG_INFO("Turning off screen"); + dispdev->displayOff(); +#ifdef USE_ST7789 + 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); +#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); +#endif +#endif + +#ifdef T_WATCH_S3 + PMU->disablePowerOutput(XPOWERS_ALDO2); +#endif + enabled = false; + } + screenOn = on; + } +} + +void Screen::setup() +{ + // We don't set useDisplay until setup() is called, because some boards have a declaration of this object but the device + // is never found when probing i2c and therefore we don't call setup and never want to do (invalid) accesses to this device. + useDisplay = true; + +#ifdef AutoOLEDWire_h + if (isAUTOOled) + static_cast(dispdev)->setDetected(model); +#endif + +#ifdef USE_SH1107_128_64 + static_cast(dispdev)->setSubtype(7); +#endif + +#if defined(USE_ST7789) && defined(TFT_MESH) + // Heltec T114 and T190: honor a custom text color, if defined in variant.h + static_cast(dispdev)->setRGB(TFT_MESH); +#endif + + // Initialising the UI will init the display too. + ui->init(); + + displayWidth = dispdev->width(); + displayHeight = dispdev->height(); + + ui->setTimePerTransition(0); + + ui->setIndicatorPosition(BOTTOM); + // Defines where the first frame is located in the bar. + ui->setIndicatorDirection(LEFT_RIGHT); + ui->setFrameAnimation(SLIDE_LEFT); + // Don't show the page swipe dots while in boot screen. + ui->disableAllIndicators(); + // Store a pointer to Screen so we can get to it from static functions. + ui->getUiState()->userData = this; + + // Set the utf8 conversion function + dispdev->setFontTableLookupFunction(customFontTableLookup); + + // Add frames. + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); + alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { +#ifdef ARCH_ESP32 + if (wakeCause == ESP_SLEEP_WAKEUP_TIMER || wakeCause == ESP_SLEEP_WAKEUP_EXT1) { + drawFrameText(display, state, x, y, "Resuming..."); + } else +#endif + { + // Draw region in upper left + const char *region = myRegion ? myRegion->name : NULL; + drawIconScreen(region, display, state, x, y); + } + }; + ui->setFrames(alertFrames, 1); + // No overlays. + ui->setOverlays(nullptr, 0); + + // Require presses to switch between frames. + ui->disableAutoTransition(); + + // Set up a log buffer with 3 lines, 32 chars each. + dispdev->setLogBuffer(3, 32); + +#ifdef SCREEN_MIRROR + dispdev->mirrorScreen(); +#else + // Standard behaviour is to FLIP the screen (needed on T-Beam). If this config item is set, unflip it, and thereby logically + // flip it. If you have a headache now, you're welcome. + if (!config.display.flip_screen) { +#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || \ + defined(ST7789_CS) || defined(RAK14014) || defined(HX8357_CS) + static_cast(dispdev)->flipScreenVertically(); +#elif defined(USE_ST7789) + static_cast(dispdev)->flipScreenVertically(); +#else + dispdev->flipScreenVertically(); +#endif + } +#endif + + // Get our hardware ID + uint8_t dmac[6]; + getMacAddr(dmac); + snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); +#if ARCH_PORTDUINO + handleSetOn(false); // force clean init +#endif + + // Turn on the display. + handleSetOn(true); + + // On some ssd1306 clones, the first draw command is discarded, so draw it + // twice initially. Skip this for EINK Displays to save a few seconds during boot + ui->update(); +#ifndef USE_EINK + ui->update(); +#endif + serialSinceMsec = millis(); + +#if ARCH_PORTDUINO + if (settingsMap[touchscreenModule]) { + touchScreenImpl1 = + new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); + touchScreenImpl1->init(); + } +#elif HAS_TOUCHSCREEN + touchScreenImpl1 = + new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); + touchScreenImpl1->init(); +#endif + + // Subscribe to status updates + powerStatusObserver.observe(&powerStatus->onNewStatus); + gpsStatusObserver.observe(&gpsStatus->onNewStatus); + nodeStatusObserver.observe(&nodeStatus->onNewStatus); + adminMessageObserver.observe(adminModule); + if (textMessageModule) + textMessageObserver.observe(textMessageModule); + if (inputBroker) + inputObserver.observe(inputBroker); + + // Modules can notify screen about refresh + MeshModule::observeUIEvents(&uiFrameEventObserver); +} + +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); + + // 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(); +#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(); + } + + // 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; + } + +#ifndef DISABLE_WELCOME_UNSET + if (showingNormalScreen && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + setWelcomeFrames(); + } +#endif + + // 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: + handleOnPress(); + break; + case Cmd::SHOW_PREV_FRAME: + handleShowPrevFrame(); + break; + case Cmd::SHOW_NEXT_FRAME: + handleShowNextFrame(); + 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; + 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?) +#endif + setFrameImmediateDraw(alertFrames); + break; + } + case Cmd::START_FIRMWARE_UPDATE_SCREEN: + handleStartFirmwareUpdateScreen(); + break; + case Cmd::STOP_ALERT_FRAME: + case Cmd::STOP_BOOT_SCREEN: + EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame + setFrames(); + break; + case Cmd::PRINT: + handlePrint(cmd.print_text); + free(cmd.print_text); + 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; + } + + // 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. + if (targetFramerate != IDLE_FRAMERATE && ui->getUiState()->frameState == FIXED) { + // oldFrameState = ui->getUiState()->frameState; + targetFramerate = IDLE_FRAMERATE; + + 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 && + !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); +#endif + + LOG_DEBUG("LastScreenTransition exceeded %ums transitioning 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); +} + +void Screen::drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + Screen *screen2 = reinterpret_cast(state->userData); + screen2->debugInfo.drawFrame(display, state, x, y); +} + +void Screen::drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + Screen *screen2 = reinterpret_cast(state->userData); + screen2->debugInfo.drawFrameSettings(display, state, x, y); +} + +void Screen::drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + Screen *screen2 = reinterpret_cast(state->userData); + screen2->debugInfo.drawFrameWiFi(display, state, x, y); +} + +/* 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("showing SSL frames"); + static FrameCallback sslFrames[] = {drawSSLScreen}; + ui->setFrames(sslFrames, 1); + ui->update(); + } +} + +/* show a message that the SSL cert is being built + * it is expected that this will be used during the boot phase */ +void Screen::setWelcomeFrames() +{ + if (address_found.address) { + // LOG_DEBUG("showing Welcome frames"); + static FrameCallback frames[] = {drawWelcomeScreen}; + setFrameImmediateDraw(frames); + } +} + +#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; + +#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); +#endif + + // 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 = 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); + + // Old EInkDisplay class +#if !defined(USE_EINK_DYNAMICDISPLAY) + 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 + + // 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" +#else + 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) +{ + uint8_t originalPosition = ui->getUiState()->currentFrame; + FramesetInfo fsi; // Location of specific frames, for applying focus parameter + + LOG_DEBUG("showing standard frames"); + showingNormalScreen = true; + +#ifdef USE_EINK + // If user has disabled the screensaver, warn them after boot + static bool warnedScreensaverDisabled = false; + if (config.display.screen_on_secs == 0 && !warnedScreensaverDisabled) { + screen->print("Screensaver disabled\n"); + warnedScreensaverDisabled = true; + } +#endif + + moduleFrames = MeshModule::GetMeshModulesWithUIFrames(); + LOG_DEBUG("Showing %d module frames", moduleFrames.size()); +#ifdef DEBUG_PORT + int totalFrameCount = MAX_NUM_NODES + NUM_EXTRA_FRAMES + moduleFrames.size(); + LOG_DEBUG("Total frame count: %d", totalFrameCount); +#endif + + // 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--; + + size_t numframes = 0; + + // put all of the module frames first. + // this is a little bit of a dirty hack; since we're going to call + // the same drawModuleFrame handler here for all of these module frames + // and then we'll just assume that the state->currentFrame value + // is the same offset into the moduleFrames vector + // so that we can invoke the module's callback + for (auto i = moduleFrames.begin(); i != moduleFrames.end(); ++i) { + // Draw the module frame, using the hack described above + 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->isRequestingFocus()) { + fsi.positions.focusedModule = numframes; + } + + // Identify the position of specific modules, if we need to know this later + if (m == waypointModule) + fsi.positions.waypoint = numframes; + + numframes++; + } + + LOG_DEBUG("Added modules. numframes: %d", numframes); + + // If we have a critical fault, show it first + fsi.positions.fault = numframes; + if (error_code) { + normalFrames[numframes++] = drawCriticalFaultFrame; + focus = FOCUS_FAULT; // Change our "focus" parameter, to ensure we show the fault frame + } + +#ifdef T_WATCH_S3 + normalFrames[numframes++] = screen->digitalWatchFace ? &Screen::drawDigitalClockFrame : &Screen::drawAnalogClockFrame; +#endif + + // If we have a text message - show it next, unless it's a phone message and we aren't using any special modules + if (devicestate.has_rx_text_message && shouldDrawMessage(&devicestate.rx_text_message)) { + fsi.positions.textMessage = numframes; + normalFrames[numframes++] = drawTextMessageFrame; + } + + // then all the nodes + // We only show a few nodes in our scrolling list - because meshes with many nodes would have too many screens + size_t numToShow = min(numMeshNodes, 4U); + for (size_t i = 0; i < numToShow; i++) + normalFrames[numframes++] = drawNodeInfo; + + // then the debug info + // + // Since frames are basic function pointers, we have to use a helper to + // call a method on debugInfo object. + fsi.positions.log = numframes; + normalFrames[numframes++] = &Screen::drawDebugInfoTrampoline; + + // call a method on debugInfoScreen object (for more details) + fsi.positions.settings = numframes; + normalFrames[numframes++] = &Screen::drawDebugInfoSettingsTrampoline; + + fsi.positions.wifi = numframes; +#if HAS_WIFI && !defined(ARCH_PORTDUINO) + if (isWifiAvailable()) { + // call a method on debugInfoScreen object (for more details) + normalFrames[numframes++] = &Screen::drawDebugInfoWiFiTrampoline; + } +#endif + + fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE + LOG_DEBUG("Finished building frames. numframes: %d", numframes); + + ui->setFrames(normalFrames, numframes); + ui->enableAllIndicators(); + + // Add function overlay here. This can show when notifications muted, modifier key is active etc + static OverlayCallback functionOverlay[] = {drawFunctionOverlay}; + static const int functionOverlayCount = sizeof(functionOverlay) / sizeof(functionOverlay[0]); + ui->setOverlays(functionOverlay, functionOverlayCount); + + 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(0); // First frame + break; + case FOCUS_FAULT: + ui->switchToFrame(fsi.positions.fault); + break; + case FOCUS_TEXTMESSAGE: + ui->switchToFrame(fsi.positions.textMessage); + 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_PRESERVE: + // If we can identify which type of frame "originalPosition" was, can move directly to it in the new frameset + const FramesetInfo &oldFsi = this->framesetInfo; + if (originalPosition == oldFsi.positions.log) + ui->switchToFrame(fsi.positions.log); + else if (originalPosition == oldFsi.positions.settings) + ui->switchToFrame(fsi.positions.settings); + else if (originalPosition == oldFsi.positions.wifi) + ui->switchToFrame(fsi.positions.wifi); + + // If frame count has decreased + else if (fsi.frameCount < oldFsi.frameCount) { + uint8_t numDropped = oldFsi.frameCount - fsi.frameCount; + // Move n frames backwards + if (numDropped <= originalPosition) + ui->switchToFrame(originalPosition - numDropped); + // Unless that would put us "out of bounds" (< 0) + else + ui->switchToFrame(0); + } + + // If we're not sure exactly which frame we were on, at least return to the same frame number + // (node frames; module frames) + else + ui->switchToFrame(originalPosition); + + break; + } + + // Store the info about this frameset, for future setFrames calls + this->framesetInfo = fsi; + + setFastFramerate(); // Draw ASAP +} + +void Screen::setFrameImmediateDraw(FrameCallback *drawFrames) +{ + ui->disableAllIndicators(); + ui->setFrames(drawFrames, 1); + setFastFramerate(); +} + +// Dismisses the currently displayed screen frame, if possible +// Relevant for text message, waypoint, others in future? +// Triggered with a CardKB keycombo +void Screen::dismissCurrentFrame() +{ + uint8_t currentFrame = ui->getUiState()->currentFrame; + bool dismissed = false; + + if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) { + LOG_INFO("Dismissing Text Message"); + devicestate.has_rx_text_message = false; + dismissed = true; + } + + else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) { + LOG_DEBUG("Dismissing Waypoint"); + devicestate.has_rx_waypoint = false; + dismissed = true; + } + + // If we did make changes to dismiss, we now need to regenerate the frameset + if (dismissed) + setFrames(); +} + +void Screen::handleStartFirmwareUpdateScreen() +{ + LOG_DEBUG("showing firmware screen"); + showingNormalScreen = false; + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // E-Ink: Explicitly use fast-refresh for next frame + + static FrameCallback frames[] = {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::setFunctionSymbal(std::string sym) +{ + if (std::find(functionSymbals.begin(), functionSymbals.end(), sym) == functionSymbals.end()) { + functionSymbals.push_back(sym); + functionSymbalString = ""; + for (auto symbol : functionSymbals) { + functionSymbalString = symbol + " " + functionSymbalString; + } + setFastFramerate(); + } +} + +void Screen::removeFunctionSymbal(std::string sym) +{ + functionSymbals.erase(std::remove(functionSymbals.begin(), functionSymbals.end(), sym), functionSymbals.end()); + functionSymbalString = ""; + for (auto symbol : functionSymbals) { + functionSymbalString = symbol + " " + functionSymbalString; + } + setFastFramerate(); +} + +std::string Screen::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; +} + +void Screen::handlePrint(const char *text) +{ + // the string passed into us probably has a newline, but that would confuse the logging system + // so strip it + LOG_DEBUG("Screen: %.*s", strlen(text) - 1, text); + if (!useDisplay || !showingNormalScreen) + return; + + dispdev->print(text); +} + +void Screen::handleOnPress() +{ + // If Canned Messages is using the "Scan and Select" input, dismiss the canned message frame when user button is pressed + // Minimize impact as a courtesy, as "scan and select" may be used as default config for some boards + if (scanAndSelectInput != nullptr && scanAndSelectInput->dismissCannedMessageFrame()) + return; + + // 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::handleShowPrevFrame() +{ + // If screen was off, just wake it, otherwise go back to previous frame + // If we are in a transition, the press must have bounced, drop it. + if (ui->getUiState()->frameState == FIXED) { + ui->previousFrame(); + lastScreenTransition = millis(); + setFastFramerate(); + } +} + +void Screen::handleShowNextFrame() +{ + // 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(); + } +} + +#ifndef SCREEN_TRANSITION_FRAMERATE +#define SCREEN_TRANSITION_FRAMERATE 30 // fps +#endif + +void Screen::setFastFramerate() +{ + // We are about to start a transition so speed up fps + targetFramerate = SCREEN_TRANSITION_FRAMERATE; + + ui->setTargetFPS(targetFramerate); + setInterval(0); // redraw ASAP + runASAP = true; +} + +void DebugInfo::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); + + 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]; + { + concurrency::LockGuard guard(&lock); + snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex())); + } + + // Display power status + if (powerStatus->getHasBattery()) { + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { + drawBattery(display, x, y + 2, imgBattery, powerStatus); + } else { + drawBattery(display, x + 1, y + 3, imgBattery, powerStatus); + } + } else if (powerStatus->knowsUSB()) { + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { + display->drawFastImage(x, y + 2, 16, 8, powerStatus->getHasUSB() ? imgUSB : imgPower); + } else { + display->drawFastImage(x + 1, y + 3, 16, 8, powerStatus->getHasUSB() ? imgUSB : imgPower); + } + } + // Display nodes status + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { + drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodeStatus); + } else { + 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) { + drawGPSpowerstat(display, x, y + 2, gpsStatus); + } else { + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { + drawGPS(display, x + (SCREEN_WIDTH * 0.63), y + 2, gpsStatus); + } else { + 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) { +#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(HX8357_CS) || ARCH_PORTDUINO) && \ + !defined(DISPLAY_FORCE_SMALL_FONTS) + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, + imgQuestionL1); + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8, + imgQuestionL2); +#else + display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(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(HX8357_CS)) && \ + !defined(DISPLAY_FORCE_SMALL_FONTS) + display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(ourId), y + 3 + FONT_HEIGHT_SMALL, 16, 8, + imgSFL1); + display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(ourId), y + 11 + FONT_HEIGHT_SMALL, 16, 8, + imgSFL2); +#else + display->drawFastImage(x + SCREEN_WIDTH - 13 - display->getStringWidth(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(HX8357_CS) || ARCH_PORTDUINO) && \ + !defined(DISPLAY_FORCE_SMALL_FONTS) + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, + imgInfoL1); + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8, + imgInfoL2); +#else + display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(ourId), y + 2 + FONT_HEIGHT_SMALL, 8, 8, imgInfo); +#endif + } + + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(ourId), y + FONT_HEIGHT_SMALL, ourId); + + // Draw any log messages + display->drawLogBuffer(x, y + (FONT_HEIGHT_SMALL * 2)); + + /* Display a heartbeat pixel that blinks every time the frame is redrawn */ +#ifdef SHOW_REDRAWS + if (heartbeat) + display->setPixel(0, 0); + heartbeat = !heartbeat; +#endif +} + +// Jm +void DebugInfo::drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ +#if HAS_WIFI && !defined(ARCH_PORTDUINO) + const char *wifiName = config.network.wifi_ssid; + + display->setFont(FONT_SMALL); + + // 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 (WiFi.status() != WL_CONNECTED) { + display->drawString(x, y, String("WiFi: Not Connected")); + if (config.display.heading_bold) + display->drawString(x + 1, y, String("WiFi: Not Connected")); + } else { + display->drawString(x, y, String("WiFi: Connected")); + if (config.display.heading_bold) + display->drawString(x + 1, y, String("WiFi: Connected")); + + display->drawString(x + SCREEN_WIDTH - display->getStringWidth("RSSI " + String(WiFi.RSSI())), y, + "RSSI " + String(WiFi.RSSI())); + if (config.display.heading_bold) { + display->drawString(x + SCREEN_WIDTH - display->getStringWidth("RSSI " + String(WiFi.RSSI())) - 1, y, + "RSSI " + String(WiFi.RSSI())); + } + } + + display->setColor(WHITE); + + /* + - 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) { + display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "IP: " + String(WiFi.localIP().toString().c_str())); + } else if (WiFi.status() == WL_NO_SSID_AVAIL) { + display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "SSID Not Found"); + } else if (WiFi.status() == WL_CONNECTION_LOST) { + display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "Connection Lost"); + } else if (WiFi.status() == WL_CONNECT_FAILED) { + display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "Connection Failed"); + } else if (WiFi.status() == WL_IDLE_STATUS) { + display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "Idle ... Reconnecting"); + } +#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, y + FONT_HEIGHT_SMALL * 1, + WiFi.disconnectReasonName(static_cast(getWifiDisconnectReason()))); + } +#else + else { + display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "Unkown status: " + String(WiFi.status())); + } +#endif + + display->drawString(x, y + FONT_HEIGHT_SMALL * 2, "SSID: " + String(wifiName)); + + display->drawString(x, y + FONT_HEIGHT_SMALL * 3, "http://meshtastic.local"); + + /* Display a heartbeat pixel that blinks every time the frame is redrawn */ +#ifdef SHOW_REDRAWS + if (heartbeat) + display->setPixel(0, 0); + heartbeat = !heartbeat; +#endif +#endif +} + +void DebugInfo::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); + + 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, String("USB")); + if (config.display.heading_bold) + display->drawString(x + 1, y, String("USB")); + } + + auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, true); + + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode), y, mode); + if (config.display.heading_bold) + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode) - 1, y, mode); + + // Line 2 + 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; + + display->setColor(WHITE); + + // Show uptime as days, hours, minutes OR seconds + std::string uptime = screen->drawTimeDelta(days, hours, minutes, seconds); + + 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 = 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 + + char timebuf[10]; + snprintf(timebuf, sizeof(timebuf), " %02d:%02d:%02d", hour, min, sec); + uptime += timebuf; + } + + display->drawString(x, y + FONT_HEIGHT_SMALL * 1, uptime.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 (config.display.gps_format != + meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude + drawGPSAltitude(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus); + + // Line 4 + drawGPScoordinates(display, x, y + FONT_HEIGHT_SMALL * 3, gpsStatus); + } else { + drawGPSpowerstat(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; +#endif +} + +int Screen::handleStatusUpdate(const meshtastic::Status *arg) +{ + // LOG_DEBUG("Screen got status update %d", arg->getStatusType()); + 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; + } + + return 0; +} + +int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) +{ + if (showingNormalScreen) { + // Outgoing message + if (packet->from == 0) + setFrames(FOCUS_PRESERVE); // Return to same frame (quietly hiding the rx text message frame) + + // Incoming message + else + setFrames(FOCUS_TEXTMESSAGE); // Focus on the new message + } + + return 0; +} + +// Triggered by MeshModules +int Screen::handleUIFrameEvent(const UIFrameEvent *event) +{ + 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(); + } + + return 0; +} + +int Screen::handleInputEvent(const InputEvent *event) +{ + +#ifdef T_WATCH_S3 + // For the T-Watch, intercept touches to the 'toggle digital/analog watch face' button + uint8_t watchFaceFrame = error_code ? 1 : 0; + + if (this->ui->getUiState()->currentFrame == watchFaceFrame && event->touchX >= 204 && event->touchX <= 240 && + event->touchY >= 204 && event->touchY <= 240) { + screen->digitalWatchFace = !screen->digitalWatchFace; + + setFrames(); + + return 0; + } +#endif + + // 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->interceptingKeyboardInput()) + inputIntercepted = true; + } + + // If no modules are using the input, move between frames + if (!inputIntercepted) { + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) + showPrevFrame(); + else if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) + showNextFrame(); + } + } + + return 0; +} + +int Screen::handleAdminMessage(const meshtastic_AdminMessage *arg) +{ + // Note: only selected admin messages notify this observer + // If you wish to handle a new type of message, you should modify AdminModule.cpp first + + switch (arg->which_payload_variant) { + // Node removed manually (i.e. via app) + case meshtastic_AdminMessage_remove_by_nodenum_tag: + setFrames(FOCUS_PRESERVE); + break; + + // Default no-op, in case the admin message observable gets used by other classes in future + default: + break; + } + return 0; +} + +} // namespace graphics +#else +graphics::Screen::Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY) {} +#endif // HAS_SCREEN \ No newline at end of file diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h new file mode 100644 index 0000000..b2e6e90 --- /dev/null +++ b/src/graphics/Screen.h @@ -0,0 +1,603 @@ +#pragma once + +#include "configuration.h" + +#include "detect/ScanI2C.h" +#include "mesh/generated/meshtastic/config.pb.h" +#include + +#if !HAS_SCREEN +#include "power.h" +namespace graphics +{ +// Noop class for boards without screen. +class Screen +{ + public: + explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); + void onPress() {} + void setup() {} + void setOn(bool) {} + void print(const char *) {} + void doDeepSleep() {} + void forceDisplay(bool forceUiUpdate = false) {} + void startFirmwareUpdateScreen() {} + void increaseBrightness() {} + void decreaseBrightness() {} + void setFunctionSymbal(std::string) {} + void removeFunctionSymbal(std::string) {} + void startAlert(const char *) {} + void endAlert() {} +}; +} // namespace graphics +#else +#include + +#include + +#include "../configuration.h" +#include "gps/GeoCoord.h" +#include "graphics/ScreenFonts.h" + +#ifdef USE_ST7567 +#include +#elif defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SH1107_128_64) +#include +#elif defined(USE_SSD1306) +#include +#elif defined(USE_ST7789) +#include +#else +// the SH1106/SSD1306 variant is auto-detected +#include +#endif + +#include "EInkDisplay2.h" +#include "EInkDynamicDisplay.h" +#include "PointStruct.h" +#include "TFTDisplay.h" +#include "TypedQueue.h" +#include "commands.h" +#include "concurrency/LockGuard.h" +#include "concurrency/OSThread.h" +#include "input/InputBroker.h" +#include "mesh/MeshModule.h" +#include "power.h" +#include + +// 0 to 255, though particular variants might define different defaults +#ifndef BRIGHTNESS_DEFAULT +#define BRIGHTNESS_DEFAULT 150 +#endif + +// Meters to feet conversion +#ifndef METERS_TO_FEET +#define METERS_TO_FEET 3.28 +#endif + +// Feet to miles conversion +#ifndef MILES_TO_FEET +#define MILES_TO_FEET 5280 +#endif + +// Intuitive colors. E-Ink display is inverted from OLED(?) +#define EINK_BLACK OLEDDISPLAY_COLOR::WHITE +#define EINK_WHITE OLEDDISPLAY_COLOR::BLACK + +// Base segment dimensions for T-Watch segmented display +#define SEGMENT_WIDTH 16 +#define SEGMENT_HEIGHT 4 + +/// Convert an integer GPS coords to a floating point +#define DegD(i) (i * 1e-7) + +namespace +{ +/// A basic 2D point class for drawing +class Point +{ + public: + float x, 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; + + x = rx; + y = ry; + } + + 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; + } +}; + +} // namespace + +namespace graphics +{ + +// Forward declarations +class Screen; + +/// Handles gathering and displaying debug information. +class DebugInfo +{ + public: + DebugInfo(const DebugInfo &) = delete; + DebugInfo &operator=(const DebugInfo &) = delete; + + private: + friend Screen; + + 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); + + /// Protects all of internal state. + concurrency::Lock lock; +}; + +/** + * @brief This class deals with showing things on the screen of the device. + * + * @details Other than setup(), this class is thread-safe as long as drawFrame is not called + * 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 textMessageObserver = + CallbackObserver(this, &Screen::handleTextMessage); + CallbackObserver uiFrameEventObserver = + CallbackObserver(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules + CallbackObserver inputObserver = + CallbackObserver(this, &Screen::handleInputEvent); + CallbackObserver adminMessageObserver = + CallbackObserver(this, &Screen::handleAdminMessage); + + public: + explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); + + ~Screen(); + + Screen(const Screen &) = delete; + Screen &operator=(const Screen &) = delete; + + ScanI2C::DeviceAddress address_found; + meshtastic_Config_DisplayConfig_OledType model; + OLEDDISPLAY_GEOMETRY geometry; + + /// 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) + { + 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}); + } + + /** + * 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 drawFrameText(OLEDDisplay *, OLEDDisplayUiState *, int16_t, int16_t, const char *); + + void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength); + + // Draw north + void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading); + + static uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight); + + float estimatedHeading(double lat, double lon); + + void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian); + + void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields); + + /// 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}); } + + // 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 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 = _heading; + } + + bool hasHeading() { return hasCompass; } + + long getHeading() { return compassHeading; } + // functions for display brightness + void increaseBrightness(); + void decreaseBrightness(); + + void setFunctionSymbal(std::string sym); + void removeFunctionSymbal(std::string sym); + + /// Stops showing the boot screen. + void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); } + + /// Writes a string to the screen. + void print(const char *text) + { + ScreenCmd cmd; + cmd.cmd = Cmd::PRINT; + // TODO(girts): strdup() here is scary, but we can't use std::string as + // FreeRTOS queue is just dumbly copying memory contents. It would be + // nice if we had a queue that could copy objects by value. + cmd.print_text = strdup(text); + if (!enqueueCmd(cmd)) { + free(cmd.print_text); + } + } + + /// generates a very brief time delta display + std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds); + + /// 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; + } + } + + // We want to strip out prefix chars for two-byte char formats + if (ch == 0xC2) + return (uint8_t)0; + +#if defined(OLED_PL) + + 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; + } + + 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; + } + } + + // 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; + } + } + + // 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 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) + } + + /// 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 *arg); + int handleUIFrameEvent(const UIFrameEvent *arg); + int handleInputEvent(const InputEvent *arg); + int handleAdminMessage(const meshtastic_AdminMessage *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(); + + void setWelcomeFrames(); + + // Dismiss the currently focussed frame, if possible (e.g. text message, waypoint) + void dismissCurrentFrame(); + +#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; + } + } + + // Implementations of various commands, called from doTask(). + void handleSetOn(bool on, FrameCallback einkScreensaver = NULL); + void handleOnPress(); + void handleShowNextFrame(); + void handleShowPrevFrame(); + void handlePrint(const char *text); + 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 = 0; + uint8_t textMessage = 0; + uint8_t waypoint = 0; + uint8_t focusedModule = 0; + uint8_t log = 0; + uint8_t settings = 0; + uint8_t wifi = 0; + } positions; + + uint8_t frameCount = 0; + } framesetInfo; + + // 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_TEXTMESSAGE, + FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus + }; + + // 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); + + /// Try to start drawing ASAP + void setFastFramerate(); + + // Sets frame up for immediate drawing + void setFrameImmediateDraw(FrameCallback *drawFrames); + + /// Called when debug screen is to be drawn, calls through to debugInfo.drawFrame. + static void drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + + static void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + + static void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +#ifdef T_WATCH_S3 + static void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + + static void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + + static void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale = 1); + + static void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height); + + static void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height); + + static void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale = 1); + + static void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode = true, float scale = 1); + + static void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y); + + // Whether we are showing the digital watch face or the analog one + bool digitalWatchFace = true; +#endif + + /// 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; + + // Implementation to Adjust Brightness + uint8_t brightness = BRIGHTNESS_DEFAULT; // H = 254, MH = 192, ML = 130 L = 103 + + bool hasCompass = false; + float compassHeading; + /// Holds state for debug information + DebugInfo debugInfo; + + /// Display device + OLEDDisplay *dispdev; + + /// UI helper for rendering to frames and switching between them + OLEDDisplayUi *ui; +}; + +} // namespace graphics + +#endif \ No newline at end of file diff --git a/src/graphics/ScreenFonts.h b/src/graphics/ScreenFonts.h new file mode 100644 index 0000000..c9ce961 --- /dev/null +++ b/src/graphics/ScreenFonts.h @@ -0,0 +1,44 @@ +#pragma once + +#ifdef OLED_PL +#include "graphics/fonts/OLEDDisplayFontsPL.h" +#endif + +#ifdef OLED_RU +#include "graphics/fonts/OLEDDisplayFontsRU.h" +#endif + +#ifdef OLED_UA +#include "graphics/fonts/OLEDDisplayFontsUA.h" +#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(DISPLAY_FORCE_SMALL_FONTS) +// The screen is bigger so use bigger fonts +#define FONT_SMALL ArialMT_Plain_16 // Height: 19 +#define FONT_MEDIUM ArialMT_Plain_24 // Height: 28 +#define FONT_LARGE ArialMT_Plain_24 // Height: 28 +#else +#ifdef OLED_PL +#define FONT_SMALL ArialMT_Plain_10_PL +#else +#ifdef OLED_RU +#define FONT_SMALL ArialMT_Plain_10_RU +#else +#ifdef OLED_UA +#define FONT_SMALL ArialMT_Plain_10_UA +#else +#define FONT_SMALL ArialMT_Plain_10 // Height: 13 +#endif +#endif +#endif +#define FONT_MEDIUM ArialMT_Plain_16 // Height: 19 +#define FONT_LARGE ArialMT_Plain_24 // Height: 28 +#endif + +#define _fontHeight(font) ((font)[1] + 1) // height is position 1 + +#define FONT_HEIGHT_SMALL _fontHeight(FONT_SMALL) +#define FONT_HEIGHT_MEDIUM _fontHeight(FONT_MEDIUM) +#define FONT_HEIGHT_LARGE _fontHeight(FONT_LARGE) diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp new file mode 100644 index 0000000..3ba847c --- /dev/null +++ b/src/graphics/TFTDisplay.cpp @@ -0,0 +1,863 @@ +#include "configuration.h" +#include "main.h" +#if ARCH_PORTDUINO +#include "platform/portduino/PortduinoGlue.h" +#endif + +#ifndef TFT_BACKLIGHT_ON +#define TFT_BACKLIGHT_ON HIGH +#endif + +#ifdef GPIO_EXTENDER +#include +#include +extern SX1509 gpioExtender; +#endif + +#ifndef TFT_MESH +#define TFT_MESH COLOR565(0x67, 0xEA, 0x94) +#endif + +#if defined(ST7735S) +#include // Graphics and font library for ST7735 driver chip + +#ifndef TFT_INVERT +#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; + + public: + LGFX(void) + { + { + 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) + + _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 = 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. + + 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); + } + +#ifdef TFT_BL + // 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 + + _light_instance.config(cfg); + _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. + } +#endif + + setPanel(&_panel_instance); + } +}; + +static LGFX *tft = nullptr; + +#elif defined(RAK14014) +#include +#include +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; +} + +#elif defined(ST7789_CS) +#include // Graphics and font library for ST7735 driver chip + +class LGFX : public lgfx::LGFX_Device +{ + lgfx::Panel_ST7789 _panel_instance; + lgfx::Bus_SPI _bus_instance; + lgfx::Light_PWM _light_instance; +#if HAS_TOUCHSCREEN +#ifdef T_WATCH_S3 + lgfx::Touch_FT5x06 _touch_instance; +#else + lgfx::Touch_GT911 _touch_instance; +#endif +#endif + + public: + LGFX(void) + { + { + 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) + + _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 = ST7789_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. + + 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.dummy_read_pixel = 9; // 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 = 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); + } + +#ifdef ST7789_BL + // 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; + + _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(); + + 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.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; +#ifdef SCREEN_TOUCH_USE_I2C1 + cfg.pin_sda = I2C_SDA1; + cfg.pin_scl = I2C_SCL1; +#else + cfg.pin_sda = I2C_SDA; + cfg.pin_scl = I2C_SCL; +#endif + // cfg.freq = 400000; + + _touch_instance.config(cfg); + _panel_instance.setTouch(&_touch_instance); + } +#endif + + setPanel(&_panel_instance); // Sets the panel to use. + } +}; + +static LGFX *tft = nullptr; + +#elif defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) + +#include // Graphics and font library for ILI9341/ILI9342 driver chip + +#if defined(ILI9341_BACKLIGHT_EN) && !defined(TFT_BL) +#define TFT_BL ILI9341_BACKLIGHT_EN +#endif + +class LGFX : public lgfx::LGFX_Device +{ +#if defined(ILI9341_DRIVER) + lgfx::Panel_ILI9341 _panel_instance; +#elif defined(ILI9342_DRIVER) + lgfx::Panel_ILI9342 _panel_instance; +#endif + lgfx::Bus_SPI _bus_instance; + lgfx::Light_PWM _light_instance; + + public: + LGFX(void) + { + { + auto cfg = _bus_instance.config(); + + // 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 +#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 +#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) + + _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 = 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. + + 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); + } + +#ifdef TFT_BL + // 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 + + _light_instance.config(cfg); + _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. + } +#endif + + setPanel(&_panel_instance); + } +}; + +static LGFX *tft = nullptr; + +#elif defined(ST7735_CS) +#include // Graphics and font library for ILI9342 driver chip + +static TFT_eSPI *tft = nullptr; // Invoke library, pins defined in User_Setup.h +#elif ARCH_PORTDUINO && HAS_SCREEN != 0 +#include // Graphics and font library for ST7735 driver chip + +class LGFX : public lgfx::LGFX_Device +{ + lgfx::Panel_LCD *_panel_instance; + lgfx::Bus_SPI _bus_instance; + + lgfx::ITouch *_touch_instance; + + public: + LGFX(void) + { + if (settingsMap[displayPanel] == st7789) + _panel_instance = new lgfx::Panel_ST7789; + else if (settingsMap[displayPanel] == st7735) + _panel_instance = new lgfx::Panel_ST7735; + else if (settingsMap[displayPanel] == st7735s) + _panel_instance = new lgfx::Panel_ST7735S; + else if (settingsMap[displayPanel] == ili9341) + _panel_instance = new lgfx::Panel_ILI9341; + else if (settingsMap[displayPanel] == ili9342) + _panel_instance = new lgfx::Panel_ILI9342; + auto buscfg = _bus_instance.config(); + buscfg.spi_mode = 0; + buscfg.spi_host = settingsMap[displayspidev]; + + buscfg.pin_dc = settingsMap[displayDC]; // Set SPI DC pin number (-1 = disable) + + _bus_instance.config(buscfg); // applies the set value to the bus. + _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("Height: %d, Width: %d ", settingsMap[displayHeight], settingsMap[displayWidth]); + cfg.pin_cs = settingsMap[displayCS]; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = settingsMap[displayReset]; + cfg.panel_width = settingsMap[displayWidth]; // actual displayable width + cfg.panel_height = settingsMap[displayHeight]; // actual displayable height + cfg.offset_x = settingsMap[displayOffsetX]; // Panel offset amount in X direction + cfg.offset_y = settingsMap[displayOffsetY]; // Panel offset amount in Y direction + cfg.offset_rotation = 0; // Rotation direction value offset 0~7 (4~7 is mirrored) + cfg.invert = settingsMap[displayInvert]; // Set to true if the light/darkness of the panel is reversed + + _panel_instance->config(cfg); + + // Configure settings for touch control. + if (settingsMap[touchscreenModule]) { + if (settingsMap[touchscreenModule] == xpt2046) { + _touch_instance = new lgfx::Touch_XPT2046; + } else if (settingsMap[touchscreenModule] == stmpe610) { + _touch_instance = new lgfx::Touch_STMPE610; + } else if (settingsMap[touchscreenModule] == ft5x06) { + _touch_instance = new lgfx::Touch_FT5x06; + } + auto touch_cfg = _touch_instance->config(); + + touch_cfg.pin_cs = settingsMap[touchscreenCS]; + touch_cfg.x_min = 0; + touch_cfg.x_max = settingsMap[displayHeight] - 1; + touch_cfg.y_min = 0; + touch_cfg.y_max = settingsMap[displayWidth] - 1; + touch_cfg.pin_int = settingsMap[touchscreenIRQ]; + touch_cfg.bus_shared = true; + touch_cfg.offset_rotation = 1; + if (settingsMap[touchscreenI2CAddr] != -1) { + touch_cfg.i2c_addr = settingsMap[touchscreenI2CAddr]; + } else { + touch_cfg.spi_host = settingsMap[touchscreenspidev]; + } + + _touch_instance->config(touch_cfg); + _panel_instance->setTouch(_touch_instance); + } + + setPanel(_panel_instance); // Sets the panel to use. + } +}; + +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; +#if defined(USE_XPT2046) + lgfx::Touch_XPT2046 _touch_instance; +#endif + + public: + LGFX(void) + { + // 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) + + _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; + +#elif defined(ST7701_CS) +#include // Graphics and font library for ST7701 driver chip +#include +#include + +class LGFX : public lgfx::LGFX_Device +{ + lgfx::Panel_ST7701 _panel_instance; + lgfx::Bus_RGB _bus_instance; + lgfx::Light_PWM _light_instance; + lgfx::Touch_FT5x06 _touch_instance; + + 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); + } +}; + +static LGFX *tft = nullptr; + +#endif + +#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ + defined(RAK14014) || defined(HX8357_CS) || (ARCH_PORTDUINO && HAS_SCREEN != 0) +#include "SPILock.h" +#include "TFTDisplay.h" +#include + +#ifdef UNPHONE +#include "unPhone.h" +extern unPhone unphone; +#endif + +GpioPin *TFTDisplay::backlightEnable = NULL; + +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); + + 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 +#endif + backlightEnable = p; + +#if ARCH_PORTDUINO + if (settingsMap[displayRotate]) { + setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayHeight], settingsMap[configNames::displayWidth]); + } else { + setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayWidth], settingsMap[configNames::displayHeight]); + } + +#elif defined(SCREEN_ROTATE) + setGeometry(GEOMETRY_RAWMODE, TFT_HEIGHT, TFT_WIDTH); +#else + setGeometry(GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); +#endif +} + +// Write the buffer to the display memory +void TFTDisplay::display(bool fromBlank) +{ + if (fromBlank) + tft->fillScreen(TFT_BLACK); + // tft->clear(); + concurrency::LockGuard g(spiLock); + + uint16_t x, y; + + for (y = 0; y < displayHeight; y++) { + for (x = 0; x < displayWidth; x++) { + auto isset = buffer[x + (y / 8) * displayWidth] & (1 << (y & 7)); + if (!fromBlank) { + // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficent + auto dblbuf_isset = buffer_back[x + (y / 8) * displayWidth] & (1 << (y & 7)); + if (isset != dblbuf_isset) { + tft->drawPixel(x, y, isset ? TFT_MESH : TFT_BLACK); + } + } else if (isset) { + tft->drawPixel(x, y, TFT_MESH); + } + } + } + // Copy the Buffer to the Back Buffer + for (y = 0; y < (displayHeight / 8); y++) { + for (x = 0; x < displayWidth; x++) { + uint16_t pos = x + y * displayWidth; + buffer_back[pos] = buffer[pos]; + } + } +} + +// 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 (settingsMap[displayBacklight] > 0) + digitalWrite(settingsMap[displayBacklight], TFT_BACKLIGHT_ON); +#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) // 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 (settingsMap[displayBacklight] > 0) + digitalWrite(settingsMap[displayBacklight], !TFT_BACKLIGHT_ON); +#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) + 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 +#else + 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) + 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) + 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("Doing TFT init"); +#ifdef RAK14014 + tft = new TFT_eSPI; +#else + tft = new LGFX; +#endif + + backlightEnable->set(true); + LOG_INFO("Power to TFT Backlight"); + +#ifdef UNPHONE + unphone.backlight(true); // using unPhone library +#endif + + tft->init(); + +#if defined(M5STACK) + 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); +#elif defined(T_DECK) || defined(PICOMPUTER_S3) || defined(CHATTER_2) + tft->setRotation(1); // T-Deck has the TFT in landscape +#elif defined(T_WATCH_S3) || defined(SENSECAP_INDICATOR) + tft->setRotation(2); // T-Watch S3 left-handed orientation +#else + tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label +#endif + tft->fillScreen(TFT_BLACK); + + return true; +} + +#endif diff --git a/src/graphics/TFTDisplay.h b/src/graphics/TFTDisplay.h new file mode 100644 index 0000000..38cd53e --- /dev/null +++ b/src/graphics/TFTDisplay.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include + +/** + * An adapter class that allows using the LovyanGFX library as if it was an OLEDDisplay implementation. + * + * Remaining TODO: + * optimize display() to only draw changed pixels (see other OLED subclasses for examples) + * Use the fast NRF52 SPI API rather than the slow standard arduino version + * + * 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); + + // Write the buffer to the display memory + virtual void display() override { display(false); }; + virtual void display(bool fromBlank); + + // 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); + + // Functions for changing display brightness + void setDisplayBrightness(uint8_t); + + /** + * 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; + + 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; + + // Connect to the display + virtual bool connect() override; +}; \ No newline at end of file diff --git a/src/graphics/fonts/OLEDDisplayFontsPL.cpp b/src/graphics/fonts/OLEDDisplayFontsPL.cpp new file mode 100644 index 0000000..03fdab5 --- /dev/null +++ b/src/graphics/fonts/OLEDDisplayFontsPL.cpp @@ -0,0 +1,440 @@ +#include "OLEDDisplayFontsPL.h" + +// Font generated or edited with the glyphEditor +const uint8_t ArialMT_Plain_10_PL[] PROGMEM = { + 0x0A, // Width: 10 + 0x0D, // Height: 13 + 0x20, // First char: 32 + 0xE0, // Number of chars: 224 + + // Jump Table: + 0xFF, 0xFF, 0x00, 0x03, // 32:65535 + 0x00, 0x00, 0x04, 0x03, // 33 + 0x00, 0x04, 0x05, 0x04, // 34 + 0x00, 0x09, 0x09, 0x06, // 35 + 0x00, 0x12, 0x0A, 0x06, // 36 + 0x00, 0x1C, 0x10, 0x09, // 37 + 0x00, 0x2C, 0x0E, 0x08, // 38 + 0x00, 0x3A, 0x01, 0x02, // 39 + 0x00, 0x3B, 0x06, 0x04, // 40 + 0x00, 0x41, 0x06, 0x04, // 41 + 0x00, 0x47, 0x05, 0x04, // 42 + 0x00, 0x4C, 0x09, 0x06, // 43 + 0x00, 0x55, 0x04, 0x03, // 44 + 0x00, 0x59, 0x03, 0x03, // 45 + 0x00, 0x5C, 0x04, 0x03, // 46 + 0x00, 0x60, 0x05, 0x04, // 47 + 0x00, 0x65, 0x0A, 0x06, // 48 + 0x00, 0x6F, 0x08, 0x05, // 49 + 0x00, 0x77, 0x0A, 0x06, // 50 + 0x00, 0x81, 0x0A, 0x06, // 51 + 0x00, 0x8B, 0x0B, 0x07, // 52 + 0x00, 0x96, 0x0A, 0x06, // 53 + 0x00, 0xA0, 0x0A, 0x06, // 54 + 0x00, 0xAA, 0x09, 0x06, // 55 + 0x00, 0xB3, 0x0A, 0x06, // 56 + 0x00, 0xBD, 0x0A, 0x06, // 57 + 0x00, 0xC7, 0x04, 0x03, // 58 + 0x00, 0xCB, 0x04, 0x03, // 59 + 0x00, 0xCF, 0x0A, 0x06, // 60 + 0x00, 0xD9, 0x09, 0x06, // 61 + 0x00, 0xE2, 0x09, 0x06, // 62 + 0x00, 0xEB, 0x0B, 0x07, // 63 + 0x00, 0xF6, 0x14, 0x0B, // 64 + 0x01, 0x0A, 0x0E, 0x08, // 65 + 0x01, 0x18, 0x0C, 0x07, // 66 + 0x01, 0x24, 0x0C, 0x07, // 67 + 0x01, 0x30, 0x0B, 0x07, // 68 + 0x01, 0x3B, 0x0C, 0x07, // 69 + 0x01, 0x47, 0x09, 0x06, // 70 + 0x01, 0x50, 0x0D, 0x08, // 71 + 0x01, 0x5D, 0x0C, 0x07, // 72 + 0x01, 0x69, 0x04, 0x03, // 73 + 0x01, 0x6D, 0x08, 0x05, // 74 + 0x01, 0x75, 0x0E, 0x08, // 75 + 0x01, 0x83, 0x0C, 0x07, // 76 + 0x01, 0x8F, 0x10, 0x09, // 77 + 0x01, 0x9F, 0x0C, 0x07, // 78 + 0x01, 0xAB, 0x0E, 0x08, // 79 + 0x01, 0xB9, 0x0B, 0x07, // 80 + 0x01, 0xC4, 0x0E, 0x08, // 81 + 0x01, 0xD2, 0x0C, 0x07, // 82 + 0x01, 0xDE, 0x0C, 0x07, // 83 + 0x01, 0xEA, 0x0B, 0x07, // 84 + 0x01, 0xF5, 0x0C, 0x07, // 85 + 0x02, 0x01, 0x0D, 0x08, // 86 + 0x02, 0x0E, 0x11, 0x0A, // 87 + 0x02, 0x1F, 0x0E, 0x08, // 88 + 0x02, 0x2D, 0x0D, 0x08, // 89 + 0x02, 0x3A, 0x0C, 0x07, // 90 + 0x02, 0x46, 0x06, 0x04, // 91 + 0x02, 0x4C, 0x06, 0x04, // 92 + 0x02, 0x52, 0x04, 0x03, // 93 + 0x02, 0x56, 0x09, 0x06, // 94 + 0x02, 0x5F, 0x0C, 0x07, // 95 + 0x02, 0x6B, 0x03, 0x03, // 96 + 0x02, 0x6E, 0x0A, 0x06, // 97 + 0x02, 0x78, 0x0A, 0x06, // 98 + 0x02, 0x82, 0x0A, 0x06, // 99 + 0x02, 0x8C, 0x0A, 0x06, // 100 + 0x02, 0x96, 0x0A, 0x06, // 101 + 0x02, 0xA0, 0x05, 0x04, // 102 + 0x02, 0xA5, 0x0A, 0x06, // 103 + 0x02, 0xAF, 0x0A, 0x06, // 104 + 0x02, 0xB9, 0x04, 0x03, // 105 + 0x02, 0xBD, 0x04, 0x03, // 106 + 0x02, 0xC1, 0x08, 0x05, // 107 + 0x02, 0xC9, 0x04, 0x03, // 108 + 0x02, 0xCD, 0x10, 0x09, // 109 + 0x02, 0xDD, 0x0A, 0x06, // 110 + 0x02, 0xE7, 0x0A, 0x06, // 111 + 0x02, 0xF1, 0x0A, 0x06, // 112 + 0x02, 0xFB, 0x0A, 0x06, // 113 + 0x03, 0x05, 0x05, 0x04, // 114 + 0x03, 0x0A, 0x08, 0x05, // 115 + 0x03, 0x12, 0x06, 0x04, // 116 + 0x03, 0x18, 0x0A, 0x06, // 117 + 0x03, 0x22, 0x09, 0x06, // 118 + 0x03, 0x2B, 0x0E, 0x08, // 119 + 0x03, 0x39, 0x0A, 0x06, // 120 + 0x03, 0x43, 0x09, 0x06, // 121 + 0x03, 0x4C, 0x0A, 0x06, // 122 + 0x03, 0x56, 0x06, 0x04, // 123 + 0x03, 0x5C, 0x04, 0x03, // 124 + 0x03, 0x60, 0x05, 0x04, // 125 + 0x03, 0x65, 0x09, 0x06, // 126 + 0xFF, 0xFF, 0x00, 0x0A, // 127 + 0xFF, 0xFF, 0x00, 0x0A, // 128 + 0x03, 0x6E, 0x0C, 0x07, // 129 + 0x03, 0x7A, 0x05, 0x04, // 130 + 0x03, 0x7F, 0x0C, 0x07, // 131 + 0x03, 0x8B, 0x0E, 0x08, // 132 + 0x03, 0x99, 0x0C, 0x07, // 133 + 0x03, 0xA5, 0x0C, 0x07, // 134 + 0x03, 0xB1, 0x0A, 0x06, // 135 + 0x03, 0xBB, 0x0A, 0x06, // 136 + 0x03, 0xC5, 0x0A, 0x06, // 137 + 0xFF, 0xFF, 0x00, 0x0A, // 138 + 0xFF, 0xFF, 0x00, 0x0A, // 139 + 0xFF, 0xFF, 0x00, 0x0A, // 140 + 0xFF, 0xFF, 0x00, 0x0A, // 141 + 0xFF, 0xFF, 0x00, 0x0A, // 142 + 0xFF, 0xFF, 0x00, 0x0A, // 143 + 0xFF, 0xFF, 0x00, 0x0A, // 144 + 0xFF, 0xFF, 0x00, 0x0A, // 145 + 0xFF, 0xFF, 0x00, 0x0A, // 146 + 0x03, 0xCF, 0x0E, 0x08, // 147 + 0x03, 0xDD, 0x0A, 0x06, // 148 + 0xFF, 0xFF, 0x00, 0x0A, // 149 + 0xFF, 0xFF, 0x00, 0x0A, // 150 + 0xFF, 0xFF, 0x00, 0x0A, // 151 + 0x03, 0xE7, 0x0C, 0x07, // 152 + 0x03, 0xF3, 0x0C, 0x07, // 153 + 0x03, 0xFF, 0x0C, 0x07, // 154 + 0x04, 0x0B, 0x08, 0x05, // 155 + 0xFF, 0xFF, 0x00, 0x0A, // 156 + 0xFF, 0xFF, 0x00, 0x0A, // 157 + 0xFF, 0xFF, 0x00, 0x0A, // 158 + 0xFF, 0xFF, 0x00, 0x0A, // 159 + 0xFF, 0xFF, 0x00, 0x0A, // 160 + 0x04, 0x13, 0x04, 0x03, // 161 + 0x04, 0x17, 0x0A, 0x06, // 162 + 0x04, 0x21, 0x0C, 0x07, // 163 + 0x04, 0x2D, 0x0A, 0x06, // 164 + 0x04, 0x37, 0x0A, 0x06, // 165 + 0x04, 0x41, 0x04, 0x03, // 166 + 0x04, 0x45, 0x0A, 0x06, // 167 + 0x04, 0x4F, 0x05, 0x04, // 168 + 0x04, 0x54, 0x0D, 0x08, // 169 + 0x04, 0x61, 0x07, 0x05, // 170 + 0x04, 0x68, 0x0A, 0x06, // 171 + 0x04, 0x72, 0x09, 0x06, // 172 + 0x04, 0x7B, 0x03, 0x03, // 173 + 0x04, 0x7E, 0x0D, 0x08, // 174 + 0x04, 0x8B, 0x0B, 0x07, // 175 + 0x04, 0x96, 0x07, 0x05, // 176 + 0x04, 0x9D, 0x0A, 0x06, // 177 + 0x04, 0xA7, 0x05, 0x04, // 178 + 0x04, 0xAC, 0x05, 0x04, // 179 + 0x04, 0xB1, 0x05, 0x04, // 180 + 0x04, 0xB6, 0x0A, 0x06, // 181 + 0x04, 0xC0, 0x09, 0x06, // 182 + 0x04, 0xC9, 0x03, 0x03, // 183 + 0x04, 0xCC, 0x06, 0x04, // 184 + 0x04, 0xD2, 0x0C, 0x07, // 185 + 0x04, 0xDE, 0x07, 0x05, // 186 + 0x04, 0xE5, 0x0C, 0x07, // 187 + 0x04, 0xF1, 0x0A, 0x06, // 188 + 0x04, 0xFB, 0x10, 0x09, // 189 + 0x05, 0x0B, 0x10, 0x09, // 190 + 0x05, 0x1B, 0x0A, 0x06, // 191 + 0x05, 0x25, 0x0E, 0x08, // 192 + 0x05, 0x33, 0x0E, 0x08, // 193 + 0x05, 0x41, 0x0E, 0x08, // 194 + 0x05, 0x4F, 0x0E, 0x08, // 195 + 0x05, 0x5D, 0x0E, 0x08, // 196 + 0x05, 0x6B, 0x0E, 0x08, // 197 + 0x05, 0x79, 0x12, 0x0A, // 198 + 0x05, 0x8B, 0x0C, 0x07, // 199 + 0x05, 0x97, 0x0C, 0x07, // 200 + 0x05, 0xA3, 0x0C, 0x07, // 201 + 0x05, 0xAF, 0x0C, 0x07, // 202 + 0x05, 0xBB, 0x0C, 0x07, // 203 + 0x05, 0xC7, 0x05, 0x04, // 204 + 0x05, 0xCC, 0x04, 0x03, // 205 + 0x05, 0xD0, 0x04, 0x03, // 206 + 0x05, 0xD4, 0x05, 0x04, // 207 + 0x05, 0xD9, 0x0B, 0x07, // 208 + 0x05, 0xE4, 0x0C, 0x07, // 209 + 0x05, 0xF0, 0x0E, 0x08, // 210 + 0x05, 0xFE, 0x0E, 0x08, // 211 + 0x06, 0x0C, 0x0E, 0x08, // 212 + 0x06, 0x1A, 0x0E, 0x08, // 213 + 0x06, 0x28, 0x0E, 0x08, // 214 + 0x06, 0x36, 0x0A, 0x06, // 215 + 0x06, 0x40, 0x0D, 0x08, // 216 + 0x06, 0x4D, 0x0C, 0x07, // 217 + 0x06, 0x59, 0x0C, 0x07, // 218 + 0x06, 0x65, 0x0C, 0x07, // 219 + 0x06, 0x71, 0x0C, 0x07, // 220 + 0x06, 0x7D, 0x0D, 0x08, // 221 + 0x06, 0x8A, 0x0B, 0x07, // 222 + 0x06, 0x95, 0x0C, 0x07, // 223 + 0x06, 0xA1, 0x0A, 0x06, // 224 + 0x06, 0xAB, 0x0A, 0x06, // 225 + 0x06, 0xB5, 0x0A, 0x06, // 226 + 0x06, 0xBF, 0x0A, 0x06, // 227 + 0x06, 0xC9, 0x0A, 0x06, // 228 + 0x06, 0xD3, 0x0A, 0x06, // 229 + 0x06, 0xDD, 0x10, 0x09, // 230 + 0x06, 0xED, 0x0A, 0x06, // 231 + 0x06, 0xF7, 0x0A, 0x06, // 232 + 0x07, 0x01, 0x0A, 0x06, // 233 + 0x07, 0x0B, 0x0A, 0x06, // 234 + 0x07, 0x15, 0x0A, 0x06, // 235 + 0x07, 0x1F, 0x05, 0x04, // 236 + 0x07, 0x24, 0x04, 0x03, // 237 + 0x07, 0x28, 0x05, 0x04, // 238 + 0x07, 0x2D, 0x05, 0x04, // 239 + 0x07, 0x32, 0x0A, 0x06, // 240 + 0x07, 0x3C, 0x0A, 0x06, // 241 + 0x07, 0x46, 0x0A, 0x06, // 242 + 0x07, 0x50, 0x0A, 0x06, // 243 + 0x07, 0x5A, 0x0A, 0x06, // 244 + 0x07, 0x64, 0x0A, 0x06, // 245 + 0x07, 0x6E, 0x0A, 0x06, // 246 + 0x07, 0x78, 0x09, 0x06, // 247 + 0x07, 0x81, 0x0A, 0x06, // 248 + 0x07, 0x8B, 0x0A, 0x06, // 249 + 0x07, 0x95, 0x0A, 0x06, // 250 + 0x07, 0x9F, 0x0A, 0x06, // 251 + 0x07, 0xA9, 0x0A, 0x06, // 252 + 0x07, 0xB3, 0x09, 0x06, // 253 + 0x07, 0xBC, 0x0A, 0x06, // 254 + 0x07, 0xC6, 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, 0xF8, 0x03, 0x40, 0x02, 0x20, 0x02, 0x00, 0x02, 0x00, 0x02, // 129 + 0x40, 0x00, 0xF8, 0x03, 0x20, // 130 + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x44, 0x00, 0x82, 0x01, 0xF8, 0x03, // 131 + 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x05, 0x00, 0x0A, // 132 + 0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x07, 0x00, 0x08, // 133 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x0A, 0x02, 0x09, 0x02, 0x10, 0x01, // 134 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x28, 0x02, 0x44, 0x01, // 135 + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x28, 0x00, 0xC4, 0x03, // 136 + 0x20, 0x02, 0x20, 0x03, 0xA8, 0x02, 0x64, 0x02, 0x20, 0x02, // 137 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x0A, 0x02, 0x09, 0x02, 0xF0, 0x01, // 147 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x28, 0x02, 0xC4, 0x01, // 148 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x06, 0x48, 0x0A, // 152 + 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x06, 0x00, 0x08, // 153 + 0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x4A, 0x02, 0x49, 0x02, 0x90, 0x01, // 154 + 0x40, 0x02, 0xA0, 0x02, 0xA8, 0x02, 0x24, 0x01, // 155 + 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 + 0x08, 0x03, 0x88, 0x02, 0xCA, 0x02, 0x69, 0x02, 0x38, 0x02, 0x18, 0x02, // 185 + 0x30, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 186 + 0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x6A, 0x02, 0x38, 0x02, 0x18, 0x02, // 187 + 0x20, 0x02, 0x20, 0x03, 0xA8, 0x02, 0x60, 0x02, 0x20, 0x02, // 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 +}; \ No newline at end of file diff --git a/src/graphics/fonts/OLEDDisplayFontsPL.h b/src/graphics/fonts/OLEDDisplayFontsPL.h new file mode 100644 index 0000000..59dd92c --- /dev/null +++ b/src/graphics/fonts/OLEDDisplayFontsPL.h @@ -0,0 +1,11 @@ +#ifndef OLEDDISPLAYFONTSPL_h +#define OLEDDISPLAYFONTSPL_h + +#ifdef ARDUINO +#include +#elif __MBED__ +#define PROGMEM +#endif + +extern const uint8_t ArialMT_Plain_10_PL[] PROGMEM; +#endif \ No newline at end of file diff --git a/src/graphics/fonts/OLEDDisplayFontsRU.cpp b/src/graphics/fonts/OLEDDisplayFontsRU.cpp new file mode 100644 index 0000000..fa055d8 --- /dev/null +++ b/src/graphics/fonts/OLEDDisplayFontsRU.cpp @@ -0,0 +1,426 @@ +#include "OLEDDisplayFontsRU.h" + +// Font generated or edited with the glyphEditor +const uint8_t ArialMT_Plain_10_RU[] PROGMEM = { + 0x0A, // Width: 10 + 0x0D, // Height: 13 + 0x20, // First char: 32 + 0xE0, // Number of chars: 224 + + // Jump Table: + 0xFF, 0xFF, 0x00, 0x0A, // 32 + 0x00, 0x00, 0x04, 0x03, // 33 + 0x00, 0x04, 0x05, 0x04, // 34 + 0x00, 0x09, 0x09, 0x06, // 35 + 0x00, 0x12, 0x0A, 0x06, // 36 + 0x00, 0x1C, 0x10, 0x09, // 37 + 0x00, 0x2C, 0x0E, 0x08, // 38 + 0x00, 0x3A, 0x01, 0x02, // 39 + 0x00, 0x3B, 0x06, 0x04, // 40 + 0x00, 0x41, 0x06, 0x04, // 41 + 0x00, 0x47, 0x05, 0x04, // 42 + 0x00, 0x4C, 0x09, 0x06, // 43 + 0x00, 0x55, 0x04, 0x03, // 44 + 0x00, 0x59, 0x03, 0x03, // 45 + 0x00, 0x5C, 0x04, 0x03, // 46 + 0x00, 0x60, 0x05, 0x04, // 47 + 0x00, 0x65, 0x0A, 0x06, // 48 + 0x00, 0x6F, 0x08, 0x05, // 49 + 0x00, 0x77, 0x0A, 0x06, // 50 + 0x00, 0x81, 0x0A, 0x06, // 51 + 0x00, 0x8B, 0x0B, 0x07, // 52 + 0x00, 0x96, 0x0A, 0x06, // 53 + 0x00, 0xA0, 0x0A, 0x06, // 54 + 0x00, 0xAA, 0x09, 0x06, // 55 + 0x00, 0xB3, 0x0A, 0x06, // 56 + 0x00, 0xBD, 0x0A, 0x06, // 57 + 0x00, 0xC7, 0x04, 0x03, // 58 + 0x00, 0xCB, 0x04, 0x03, // 59 + 0x00, 0xCF, 0x0A, 0x06, // 60 + 0x00, 0xD9, 0x09, 0x06, // 61 + 0x00, 0xE2, 0x09, 0x06, // 62 + 0x00, 0xEB, 0x0B, 0x07, // 63 + 0x00, 0xF6, 0x14, 0x0B, // 64 + 0x01, 0x0A, 0x0E, 0x08, // 65 + 0x01, 0x18, 0x0C, 0x07, // 66 + 0x01, 0x24, 0x0C, 0x07, // 67 + 0x01, 0x30, 0x0B, 0x07, // 68 + 0x01, 0x3B, 0x0C, 0x07, // 69 + 0x01, 0x47, 0x09, 0x06, // 70 + 0x01, 0x50, 0x0D, 0x08, // 71 + 0x01, 0x5D, 0x0C, 0x07, // 72 + 0x01, 0x69, 0x04, 0x03, // 73 + 0x01, 0x6D, 0x08, 0x05, // 74 + 0x01, 0x75, 0x0E, 0x08, // 75 + 0x01, 0x83, 0x0C, 0x07, // 76 + 0x01, 0x8F, 0x10, 0x09, // 77 + 0x01, 0x9F, 0x0C, 0x07, // 78 + 0x01, 0xAB, 0x0E, 0x08, // 79 + 0x01, 0xB9, 0x0B, 0x07, // 80 + 0x01, 0xC4, 0x0E, 0x08, // 81 + 0x01, 0xD2, 0x0C, 0x07, // 82 + 0x01, 0xDE, 0x0C, 0x07, // 83 + 0x01, 0xEA, 0x0B, 0x07, // 84 + 0x01, 0xF5, 0x0C, 0x07, // 85 + 0x02, 0x01, 0x0D, 0x08, // 86 + 0x02, 0x0E, 0x11, 0x0A, // 87 + 0x02, 0x1F, 0x0E, 0x08, // 88 + 0x02, 0x2D, 0x0D, 0x08, // 89 + 0x02, 0x3A, 0x0C, 0x07, // 90 + 0x02, 0x46, 0x06, 0x04, // 91 + 0x02, 0x4C, 0x06, 0x04, // 92 + 0x02, 0x52, 0x04, 0x03, // 93 + 0x02, 0x56, 0x09, 0x06, // 94 + 0x02, 0x5F, 0x0C, 0x07, // 95 + 0x02, 0x6B, 0x03, 0x03, // 96 + 0x02, 0x6E, 0x0A, 0x06, // 97 + 0x02, 0x78, 0x0A, 0x06, // 98 + 0x02, 0x82, 0x0A, 0x06, // 99 + 0x02, 0x8C, 0x0A, 0x06, // 100 + 0x02, 0x96, 0x0A, 0x06, // 101 + 0x02, 0xA0, 0x05, 0x04, // 102 + 0x02, 0xA5, 0x0A, 0x06, // 103 + 0x02, 0xAF, 0x0A, 0x06, // 104 + 0x02, 0xB9, 0x04, 0x03, // 105 + 0x02, 0xBD, 0x04, 0x03, // 106 + 0x02, 0xC1, 0x08, 0x05, // 107 + 0x02, 0xC9, 0x04, 0x03, // 108 + 0x02, 0xCD, 0x10, 0x09, // 109 + 0x02, 0xDD, 0x0A, 0x06, // 110 + 0x02, 0xE7, 0x0A, 0x06, // 111 + 0x02, 0xF1, 0x0A, 0x06, // 112 + 0x02, 0xFB, 0x0A, 0x06, // 113 + 0x03, 0x05, 0x05, 0x04, // 114 + 0x03, 0x0A, 0x08, 0x05, // 115 + 0x03, 0x12, 0x06, 0x04, // 116 + 0x03, 0x18, 0x0A, 0x06, // 117 + 0x03, 0x22, 0x09, 0x06, // 118 + 0x03, 0x2B, 0x0E, 0x08, // 119 + 0x03, 0x39, 0x0A, 0x06, // 120 + 0x03, 0x43, 0x09, 0x06, // 121 + 0x03, 0x4C, 0x0A, 0x06, // 122 + 0x03, 0x56, 0x06, 0x04, // 123 + 0x03, 0x5C, 0x04, 0x03, // 124 + 0x03, 0x60, 0x05, 0x04, // 125 + 0x03, 0x65, 0x09, 0x06, // 126 + 0xFF, 0xFF, 0x00, 0x0A, // 127 + 0xFF, 0xFF, 0x00, 0x0A, // 128 + 0xFF, 0xFF, 0x00, 0x0A, // 129 + 0xFF, 0xFF, 0x00, 0x0A, // 130 + 0xFF, 0xFF, 0x00, 0x0A, // 131 + 0xFF, 0xFF, 0x00, 0x0A, // 132 + 0xFF, 0xFF, 0x00, 0x0A, // 133 + 0xFF, 0xFF, 0x00, 0x0A, // 134 + 0xFF, 0xFF, 0x00, 0x0A, // 135 + 0xFF, 0xFF, 0x00, 0x0A, // 136 + 0xFF, 0xFF, 0x00, 0x0A, // 137 + 0xFF, 0xFF, 0x00, 0x0A, // 138 + 0xFF, 0xFF, 0x00, 0x0A, // 139 + 0xFF, 0xFF, 0x00, 0x0A, // 140 + 0xFF, 0xFF, 0x00, 0x0A, // 141 + 0xFF, 0xFF, 0x00, 0x0A, // 142 + 0xFF, 0xFF, 0x00, 0x0A, // 143 + 0xFF, 0xFF, 0x00, 0x0A, // 144 + 0xFF, 0xFF, 0x00, 0x0A, // 145 + 0xFF, 0xFF, 0x00, 0x0A, // 146 + 0xFF, 0xFF, 0x00, 0x0A, // 147 + 0xFF, 0xFF, 0x00, 0x0A, // 148 + 0xFF, 0xFF, 0x00, 0x0A, // 149 + 0xFF, 0xFF, 0x00, 0x0A, // 150 + 0xFF, 0xFF, 0x00, 0x0A, // 151 + 0xFF, 0xFF, 0x00, 0x0A, // 152 + 0xFF, 0xFF, 0x00, 0x0A, // 153 + 0xFF, 0xFF, 0x00, 0x0A, // 154 + 0xFF, 0xFF, 0x00, 0x0A, // 155 + 0xFF, 0xFF, 0x00, 0x0A, // 156 + 0xFF, 0xFF, 0x00, 0x0A, // 157 + 0xFF, 0xFF, 0x00, 0x0A, // 158 + 0xFF, 0xFF, 0x00, 0x0A, // 159 + 0xFF, 0xFF, 0x00, 0x0A, // 160 + 0x03, 0x6E, 0x04, 0x03, // 161 + 0x03, 0x72, 0x0A, 0x06, // 162 + 0x03, 0x7C, 0x0C, 0x07, // 163 + 0x03, 0x88, 0x0A, 0x06, // 164 + 0x03, 0x92, 0x0A, 0x06, // 165 + 0x03, 0x9C, 0x04, 0x03, // 166 + 0x03, 0xA0, 0x0A, 0x06, // 167 + 0x03, 0xAA, 0x0C, 0x07, // 168 + 0x03, 0xB6, 0x0D, 0x08, // 169 + 0x03, 0xC3, 0x07, 0x05, // 170 + 0x03, 0xCA, 0x0A, 0x06, // 171 + 0x03, 0xD4, 0x09, 0x06, // 172 + 0x03, 0xDD, 0x03, 0x03, // 173 + 0x03, 0xE0, 0x0D, 0x08, // 174 + 0x03, 0xED, 0x0B, 0x07, // 175 + 0x03, 0xF8, 0x07, 0x05, // 176 + 0x03, 0xFF, 0x0A, 0x06, // 177 + 0x04, 0x09, 0x05, 0x04, // 178 + 0x04, 0x0E, 0x05, 0x04, // 179 + 0x04, 0x13, 0x05, 0x04, // 180 + 0x04, 0x18, 0x0A, 0x06, // 181 + 0x04, 0x22, 0x09, 0x06, // 182 + 0x04, 0x2B, 0x03, 0x03, // 183 + 0x04, 0x2E, 0x0B, 0x07, // 184 + 0x04, 0x39, 0x0B, 0x07, // 185 + 0x04, 0x44, 0x07, 0x05, // 186 + 0x04, 0x4B, 0x0A, 0x06, // 187 + 0x04, 0x55, 0x10, 0x09, // 188 + 0x04, 0x65, 0x10, 0x09, // 189 + 0x04, 0x75, 0x10, 0x09, // 190 + 0x04, 0x85, 0x0A, 0x06, // 191 + 0x04, 0x8F, 0x0C, 0x07, // 192 + 0x04, 0x9B, 0x0C, 0x07, // 193 + 0x04, 0xA7, 0x0C, 0x07, // 194 + 0x04, 0xB3, 0x0B, 0x07, // 195 + 0x04, 0xBE, 0x0C, 0x07, // 196 + 0x04, 0xCA, 0x0C, 0x07, // 197 + 0x04, 0xD6, 0x0C, 0x07, // 198 + 0x04, 0xE2, 0x0C, 0x07, // 199 + 0x04, 0xEE, 0x0C, 0x07, // 200 + 0x04, 0xFA, 0x0C, 0x07, // 201 + 0x05, 0x06, 0x0C, 0x07, // 202 + 0x05, 0x12, 0x0C, 0x07, // 203 + 0x05, 0x1E, 0x0C, 0x07, // 204 + 0x05, 0x2A, 0x0C, 0x07, // 205 + 0x05, 0x36, 0x0C, 0x07, // 206 + 0x05, 0x42, 0x0C, 0x07, // 207 + 0x05, 0x4E, 0x0B, 0x07, // 208 + 0x05, 0x59, 0x0C, 0x07, // 209 + 0x05, 0x65, 0x0B, 0x07, // 210 + 0x05, 0x70, 0x0C, 0x07, // 211 + 0x05, 0x7C, 0x0B, 0x07, // 212 + 0x05, 0x87, 0x0C, 0x07, // 213 + 0x05, 0x93, 0x0C, 0x07, // 214 + 0x05, 0x9F, 0x0C, 0x07, // 215 + 0x05, 0xAB, 0x0C, 0x07, // 216 + 0x05, 0xB7, 0x0E, 0x08, // 217 + 0x05, 0xC5, 0x0C, 0x07, // 218 + 0x05, 0xD1, 0x0C, 0x07, // 219 + 0x05, 0xDD, 0x0C, 0x07, // 220 + 0x05, 0xE9, 0x0C, 0x07, // 221 + 0x05, 0xF5, 0x0C, 0x07, // 222 + 0x06, 0x01, 0x0C, 0x07, // 223 + 0x06, 0x0D, 0x0C, 0x07, // 224 + 0x06, 0x19, 0x0C, 0x07, // 225 + 0x06, 0x25, 0x0C, 0x07, // 226 + 0x06, 0x31, 0x0B, 0x07, // 227 + 0x06, 0x3C, 0x0C, 0x07, // 228 + 0x06, 0x48, 0x0B, 0x07, // 229 + 0x06, 0x53, 0x0C, 0x07, // 230 + 0x06, 0x5F, 0x0C, 0x07, // 231 + 0x06, 0x6B, 0x0C, 0x07, // 232 + 0x06, 0x77, 0x0C, 0x07, // 233 + 0x06, 0x83, 0x0C, 0x07, // 234 + 0x06, 0x8F, 0x0C, 0x07, // 235 + 0x06, 0x9B, 0x0C, 0x07, // 236 + 0x06, 0xA7, 0x0C, 0x07, // 237 + 0x06, 0xB3, 0x0C, 0x07, // 238 + 0x06, 0xBF, 0x0C, 0x07, // 239 + 0x06, 0xCB, 0x0B, 0x07, // 240 + 0x06, 0xD6, 0x0C, 0x07, // 241 + 0x06, 0xE2, 0x0B, 0x07, // 242 + 0x06, 0xED, 0x0C, 0x07, // 243 + 0x06, 0xF9, 0x0B, 0x07, // 244 + 0x07, 0x04, 0x0C, 0x07, // 245 + 0x07, 0x10, 0x0C, 0x07, // 246 + 0x07, 0x1C, 0x0C, 0x07, // 247 + 0x07, 0x28, 0x0C, 0x07, // 248 + 0x07, 0x34, 0x0E, 0x08, // 249 + 0x07, 0x42, 0x0C, 0x07, // 250 + 0x07, 0x4E, 0x0C, 0x07, // 251 + 0x07, 0x5A, 0x0C, 0x07, // 252 + 0x07, 0x66, 0x0C, 0x07, // 253 + 0x07, 0x72, 0x0C, 0x07, // 254 + 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 +}; \ No newline at end of file diff --git a/src/graphics/fonts/OLEDDisplayFontsRU.h b/src/graphics/fonts/OLEDDisplayFontsRU.h new file mode 100644 index 0000000..7510dcd --- /dev/null +++ b/src/graphics/fonts/OLEDDisplayFontsRU.h @@ -0,0 +1,11 @@ +#ifndef OLEDDISPLAYFONTSRU_h +#define OLEDDISPLAYFONTSRU_h + +#ifdef ARDUINO +#include +#elif __MBED__ +#define PROGMEM +#endif + +extern const uint8_t ArialMT_Plain_10_RU[] PROGMEM; +#endif \ No newline at end of file diff --git a/src/graphics/fonts/OLEDDisplayFontsUA.cpp b/src/graphics/fonts/OLEDDisplayFontsUA.cpp new file mode 100644 index 0000000..0295ee6 --- /dev/null +++ b/src/graphics/fonts/OLEDDisplayFontsUA.cpp @@ -0,0 +1,424 @@ +#include "OLEDDisplayFontsUA.h" + +// Font generated or edited with the glyphEditor +const uint8_t ArialMT_Plain_10_UA[] PROGMEM = { + 0x0A, // Width: 10 + 0x0D, // Height: 13 + 0x20, // First char: 32 + 0xE0, // Number of chars: 224 + // Jump Table: + 0xFF, 0xFF, 0x00, 0x0A, // 32 + 0x00, 0x00, 0x04, 0x03, // 33 + 0x00, 0x04, 0x05, 0x04, // 34 + 0x00, 0x09, 0x09, 0x06, // 35 + 0x00, 0x12, 0x0A, 0x06, // 36 + 0x00, 0x1C, 0x10, 0x09, // 37 + 0x00, 0x2C, 0x0E, 0x08, // 38 + 0x00, 0x3A, 0x01, 0x02, // 39 + 0x00, 0x3B, 0x06, 0x04, // 40 + 0x00, 0x41, 0x06, 0x04, // 41 + 0x00, 0x47, 0x05, 0x04, // 42 + 0x00, 0x4C, 0x09, 0x06, // 43 + 0x00, 0x55, 0x04, 0x03, // 44 + 0x00, 0x59, 0x03, 0x03, // 45 + 0x00, 0x5C, 0x04, 0x03, // 46 + 0x00, 0x60, 0x05, 0x04, // 47 + 0x00, 0x65, 0x0A, 0x06, // 48 + 0x00, 0x6F, 0x08, 0x05, // 49 + 0x00, 0x77, 0x0A, 0x06, // 50 + 0x00, 0x81, 0x0A, 0x06, // 51 + 0x00, 0x8B, 0x0B, 0x07, // 52 + 0x00, 0x96, 0x0A, 0x06, // 53 + 0x00, 0xA0, 0x0A, 0x06, // 54 + 0x00, 0xAA, 0x09, 0x06, // 55 + 0x00, 0xB3, 0x0A, 0x06, // 56 + 0x00, 0xBD, 0x0A, 0x06, // 57 + 0x00, 0xC7, 0x04, 0x03, // 58 + 0x00, 0xCB, 0x04, 0x03, // 59 + 0x00, 0xCF, 0x0A, 0x06, // 60 + 0x00, 0xD9, 0x09, 0x06, // 61 + 0x00, 0xE2, 0x09, 0x06, // 62 + 0x00, 0xEB, 0x0B, 0x07, // 63 + 0x00, 0xF6, 0x14, 0x0B, // 64 + 0x01, 0x0A, 0x0E, 0x08, // 65 + 0x01, 0x18, 0x0C, 0x07, // 66 + 0x01, 0x24, 0x0C, 0x07, // 67 + 0x01, 0x30, 0x0B, 0x07, // 68 + 0x01, 0x3B, 0x0C, 0x07, // 69 + 0x01, 0x47, 0x09, 0x06, // 70 + 0x01, 0x50, 0x0D, 0x08, // 71 + 0x01, 0x5D, 0x0C, 0x07, // 72 + 0x01, 0x69, 0x04, 0x03, // 73 + 0x01, 0x6D, 0x08, 0x05, // 74 + 0x01, 0x75, 0x0E, 0x08, // 75 + 0x01, 0x83, 0x0C, 0x07, // 76 + 0x01, 0x8F, 0x10, 0x09, // 77 + 0x01, 0x9F, 0x0C, 0x07, // 78 + 0x01, 0xAB, 0x0E, 0x08, // 79 + 0x01, 0xB9, 0x0B, 0x07, // 80 + 0x01, 0xC4, 0x0E, 0x08, // 81 + 0x01, 0xD2, 0x0C, 0x07, // 82 + 0x01, 0xDE, 0x0C, 0x07, // 83 + 0x01, 0xEA, 0x0B, 0x07, // 84 + 0x01, 0xF5, 0x0C, 0x07, // 85 + 0x02, 0x01, 0x0D, 0x08, // 86 + 0x02, 0x0E, 0x11, 0x0A, // 87 + 0x02, 0x1F, 0x0E, 0x08, // 88 + 0x02, 0x2D, 0x0D, 0x08, // 89 + 0x02, 0x3A, 0x0C, 0x07, // 90 + 0x02, 0x46, 0x06, 0x04, // 91 + 0x02, 0x4C, 0x06, 0x04, // 92 + 0x02, 0x52, 0x04, 0x03, // 93 + 0x02, 0x56, 0x09, 0x06, // 94 + 0x02, 0x5F, 0x0C, 0x07, // 95 + 0x02, 0x6B, 0x03, 0x03, // 96 + 0x02, 0x6E, 0x0A, 0x06, // 97 + 0x02, 0x78, 0x0A, 0x06, // 98 + 0x02, 0x82, 0x0A, 0x06, // 99 + 0x02, 0x8C, 0x0A, 0x06, // 100 + 0x02, 0x96, 0x0A, 0x06, // 101 + 0x02, 0xA0, 0x05, 0x04, // 102 + 0x02, 0xA5, 0x0A, 0x06, // 103 + 0x02, 0xAF, 0x0A, 0x06, // 104 + 0x02, 0xB9, 0x04, 0x03, // 105 + 0x02, 0xBD, 0x04, 0x03, // 106 + 0x02, 0xC1, 0x08, 0x05, // 107 + 0x02, 0xC9, 0x04, 0x03, // 108 + 0x02, 0xCD, 0x10, 0x09, // 109 + 0x02, 0xDD, 0x0A, 0x06, // 110 + 0x02, 0xE7, 0x0A, 0x06, // 111 + 0x02, 0xF1, 0x0A, 0x06, // 112 + 0x02, 0xFB, 0x0A, 0x06, // 113 + 0x03, 0x05, 0x05, 0x04, // 114 + 0x03, 0x0A, 0x08, 0x05, // 115 + 0x03, 0x12, 0x06, 0x04, // 116 + 0x03, 0x18, 0x0A, 0x06, // 117 + 0x03, 0x22, 0x09, 0x06, // 118 + 0x03, 0x2B, 0x0E, 0x08, // 119 + 0x03, 0x39, 0x0A, 0x06, // 120 + 0x03, 0x43, 0x09, 0x06, // 121 + 0x03, 0x4C, 0x0A, 0x06, // 122 + 0x03, 0x56, 0x06, 0x04, // 123 + 0x03, 0x5C, 0x04, 0x03, // 124 + 0x03, 0x60, 0x05, 0x04, // 125 + 0x03, 0x65, 0x09, 0x06, // 126 + 0xFF, 0xFF, 0x00, 0x0A, // 127 + 0xFF, 0xFF, 0x00, 0x0A, // 128 + 0xFF, 0xFF, 0x00, 0x0A, // 129 + 0xFF, 0xFF, 0x00, 0x0A, // 130 + 0xFF, 0xFF, 0x00, 0x0A, // 131 + 0xFF, 0xFF, 0x00, 0x0A, // 132 + 0xFF, 0xFF, 0x00, 0x0A, // 133 + 0xFF, 0xFF, 0x00, 0x0A, // 134 + 0xFF, 0xFF, 0x00, 0x0A, // 135 + 0xFF, 0xFF, 0x00, 0x0A, // 136 + 0xFF, 0xFF, 0x00, 0x0A, // 137 + 0xFF, 0xFF, 0x00, 0x0A, // 138 + 0xFF, 0xFF, 0x00, 0x0A, // 139 + 0xFF, 0xFF, 0x00, 0x0A, // 140 + 0xFF, 0xFF, 0x00, 0x0A, // 141 + 0xFF, 0xFF, 0x00, 0x0A, // 142 + 0xFF, 0xFF, 0x00, 0x0A, // 143 + 0xFF, 0xFF, 0x00, 0x0A, // 144 + 0xFF, 0xFF, 0x00, 0x0A, // 145 + 0xFF, 0xFF, 0x00, 0x0A, // 146 + 0xFF, 0xFF, 0x00, 0x0A, // 147 + 0xFF, 0xFF, 0x00, 0x0A, // 148 + 0xFF, 0xFF, 0x00, 0x0A, // 149 + 0xFF, 0xFF, 0x00, 0x0A, // 150 + 0xFF, 0xFF, 0x00, 0x0A, // 151 + 0xFF, 0xFF, 0x00, 0x0A, // 152 + 0xFF, 0xFF, 0x00, 0x0A, // 153 + 0xFF, 0xFF, 0x00, 0x0A, // 154 + 0xFF, 0xFF, 0x00, 0x0A, // 155 + 0xFF, 0xFF, 0x00, 0x0A, // 156 + 0xFF, 0xFF, 0x00, 0x0A, // 157 + 0xFF, 0xFF, 0x00, 0x0A, // 158 + 0xFF, 0xFF, 0x00, 0x0A, // 159 + 0xFF, 0xFF, 0x00, 0x0A, // 160 + 0x03, 0x6E, 0x04, 0x03, // 161 + 0x03, 0x72, 0x0A, 0x06, // 162 + 0x03, 0x7C, 0x0C, 0x07, // 163 + 0x03, 0x88, 0x0A, 0x06, // 164 + 0x03, 0x92, 0x09, 0x06, // 165 + 0x03, 0x9B, 0x04, 0x03, // 166 + 0x03, 0x9F, 0x0A, 0x06, // 167 + 0x03, 0xA9, 0x0C, 0x07, // 168 + 0x03, 0xB5, 0x0D, 0x08, // 169 + 0x03, 0xC2, 0x0C, 0x07, // 170 + 0x03, 0xCE, 0x0A, 0x06, // 171 + 0x03, 0xD8, 0x09, 0x06, // 172 + 0x03, 0xE1, 0x03, 0x03, // 173 + 0x03, 0xE4, 0x0D, 0x08, // 174 + 0x03, 0xF1, 0x0C, 0x07, // 175 + 0x03, 0xFD, 0x07, 0x05, // 176 + 0x04, 0x04, 0x0A, 0x06, // 177 + 0x04, 0x0E, 0x0C, 0x07, // 178 + 0x04, 0x1A, 0x0C, 0x07, // 179 + 0x04, 0x26, 0x07, 0x05, // 180 + 0x04, 0x2D, 0x0A, 0x06, // 181 + 0x04, 0x37, 0x09, 0x06, // 182 + 0x04, 0x40, 0x03, 0x03, // 183 + 0x04, 0x43, 0x0B, 0x07, // 184 + 0x04, 0x4E, 0x0B, 0x07, // 185 + 0x04, 0x59, 0x0C, 0x07, // 186 + 0x04, 0x65, 0x0A, 0x06, // 187 + 0x04, 0x6F, 0x10, 0x09, // 188 + 0x04, 0x7F, 0x10, 0x09, // 189 + 0x04, 0x8F, 0x10, 0x09, // 190 + 0x04, 0x9F, 0x0A, 0x06, // 191 + 0x04, 0xA9, 0x0C, 0x07, // 192 + 0x04, 0xB5, 0x0C, 0x07, // 193 + 0x04, 0xC1, 0x0C, 0x07, // 194 + 0x04, 0xCD, 0x0B, 0x07, // 195 + 0x04, 0xD8, 0x0C, 0x07, // 196 + 0x04, 0xE4, 0x0C, 0x07, // 197 + 0x04, 0xF0, 0x0C, 0x07, // 198 + 0x04, 0xFC, 0x0C, 0x07, // 199 + 0x05, 0x08, 0x0C, 0x07, // 200 + 0x05, 0x14, 0x0C, 0x07, // 201 + 0x05, 0x20, 0x0C, 0x07, // 202 + 0x05, 0x2C, 0x0C, 0x07, // 203 + 0x05, 0x38, 0x0C, 0x07, // 204 + 0x05, 0x44, 0x0C, 0x07, // 205 + 0x05, 0x50, 0x0C, 0x07, // 206 + 0x05, 0x5C, 0x0C, 0x07, // 207 + 0x05, 0x68, 0x0B, 0x07, // 208 + 0x05, 0x73, 0x0C, 0x07, // 209 + 0x05, 0x7F, 0x0B, 0x07, // 210 + 0x05, 0x8A, 0x0C, 0x07, // 211 + 0x05, 0x96, 0x0B, 0x07, // 212 + 0x05, 0xA1, 0x0C, 0x07, // 213 + 0x05, 0xAD, 0x0C, 0x07, // 214 + 0x05, 0xB9, 0x0C, 0x07, // 215 + 0x05, 0xC5, 0x0C, 0x07, // 216 + 0x05, 0xD1, 0x0E, 0x08, // 217 + 0x05, 0xDF, 0x0C, 0x07, // 218 + 0x05, 0xEB, 0x0C, 0x07, // 219 + 0x05, 0xF7, 0x0C, 0x07, // 220 + 0x06, 0x03, 0x0C, 0x07, // 221 + 0x06, 0x0F, 0x0C, 0x07, // 222 + 0x06, 0x1B, 0x0C, 0x07, // 223 + 0x06, 0x27, 0x0C, 0x07, // 224 + 0x06, 0x33, 0x0C, 0x07, // 225 + 0x06, 0x3F, 0x0C, 0x07, // 226 + 0x06, 0x4B, 0x0B, 0x07, // 227 + 0x06, 0x56, 0x0C, 0x07, // 228 + 0x06, 0x62, 0x0B, 0x07, // 229 + 0x06, 0x6D, 0x0C, 0x07, // 230 + 0x06, 0x79, 0x0C, 0x07, // 231 + 0x06, 0x85, 0x0C, 0x07, // 232 + 0x06, 0x91, 0x0C, 0x07, // 233 + 0x06, 0x9D, 0x0C, 0x07, // 234 + 0x06, 0xA9, 0x0C, 0x07, // 235 + 0x06, 0xB5, 0x0C, 0x07, // 236 + 0x06, 0xC1, 0x0C, 0x07, // 237 + 0x06, 0xCD, 0x0C, 0x07, // 238 + 0x06, 0xD9, 0x0C, 0x07, // 239 + 0x06, 0xE5, 0x0B, 0x07, // 240 + 0x06, 0xF0, 0x0C, 0x07, // 241 + 0x06, 0xFC, 0x0B, 0x07, // 242 + 0x07, 0x07, 0x0C, 0x07, // 243 + 0x07, 0x13, 0x0B, 0x07, // 244 + 0x07, 0x1E, 0x0C, 0x07, // 245 + 0x07, 0x2A, 0x0C, 0x07, // 246 + 0x07, 0x36, 0x0C, 0x07, // 247 + 0x07, 0x42, 0x0C, 0x07, // 248 + 0x07, 0x4E, 0x0E, 0x08, // 249 + 0x07, 0x5C, 0x0C, 0x07, // 250 + 0x07, 0x68, 0x0C, 0x07, // 251 + 0x07, 0x74, 0x0C, 0x07, // 252 + 0x07, 0x80, 0x0C, 0x07, // 253 + 0x07, 0x8C, 0x0C, 0x07, // 254 + 0x07, 0x98, 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 + 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 + 0x00, 0x00, 0x00, 0x00, 0x28, 0x02, 0xE0, 0x03, 0x28, 0x02, // 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 +}; \ No newline at end of file diff --git a/src/graphics/fonts/OLEDDisplayFontsUA.h b/src/graphics/fonts/OLEDDisplayFontsUA.h new file mode 100644 index 0000000..3bd9bb4 --- /dev/null +++ b/src/graphics/fonts/OLEDDisplayFontsUA.h @@ -0,0 +1,11 @@ +#ifndef OLEDDISPLAYFONTSUA_h +#define OLEDDISPLAYFONTSUA_h + +#ifdef ARDUINO +#include +#elif __MBED__ +#define PROGMEM +#endif + +extern const uint8_t ArialMT_Plain_10_UA[] PROGMEM; +#endif \ No newline at end of file diff --git a/src/graphics/images.h b/src/graphics/images.h new file mode 100644 index 0000000..2b0854a --- /dev/null +++ b/src/graphics/images.h @@ -0,0 +1,207 @@ +#pragma once + +#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 imgSatellite[] PROGMEM = {0x70, 0x71, 0x22, 0xFA, 0xFA, 0x22, 0x71, 0x70}; +const uint8_t imgUSB[] PROGMEM = {0x60, 0x60, 0x30, 0x18, 0x18, 0x18, 0x24, 0x42, 0x42, 0x42, 0x42, 0x7E, 0x24, 0x24, 0x24, 0x3C}; +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}; + +#ifdef T_WATCH_S3 +const uint8_t bluetoothConnectedIcon[36] PROGMEM = {0xfe, 0x01, 0xff, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x00, 0xe3, 0x1f, + 0xf3, 0x3f, 0x33, 0x30, 0x33, 0x33, 0x33, 0x33, 0x03, 0x33, 0xff, 0x33, + 0xfe, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0x3f, 0xe0, 0x1f}; +#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) || 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}; +#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}; +const uint8_t imgSF[] PROGMEM = {0xd2, 0xb7, 0xad, 0xbb, 0x92, 0x01, 0xfd, 0xfd, 0x15, 0x85, 0xf5}; +#endif + +#ifndef EXCLUDE_EMOJI +#define thumbs_height 25 +#define thumbs_width 25 +static unsigned char thumbup[] PROGMEM = { + 0x00, 0x1C, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x80, 0x09, 0x00, 0x00, + 0xC0, 0x08, 0x00, 0x00, 0x40, 0x08, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, + 0x0C, 0xCE, 0x7F, 0x00, 0x04, 0x20, 0x80, 0x00, 0x02, 0x20, 0x80, 0x00, 0x02, 0x60, 0xC0, 0x00, 0x01, 0xF8, 0xFF, 0x01, + 0x01, 0x08, 0x00, 0x01, 0x01, 0x08, 0x00, 0x01, 0x01, 0xF8, 0xFF, 0x00, 0x01, 0x10, 0x80, 0x00, 0x01, 0x18, 0x80, 0x00, + 0x02, 0x30, 0xC0, 0x00, 0x06, 0xE0, 0x3F, 0x00, 0x0C, 0x20, 0x30, 0x00, 0x38, 0x20, 0x10, 0x00, 0xE0, 0xCF, 0x1F, 0x00, +}; + +static unsigned char thumbdown[] PROGMEM = { + 0xE0, 0xCF, 0x1F, 0x00, 0x38, 0x20, 0x10, 0x00, 0x0C, 0x20, 0x30, 0x00, 0x06, 0xE0, 0x3F, 0x00, 0x02, 0x30, 0xC0, 0x00, + 0x01, 0x18, 0x80, 0x00, 0x01, 0x10, 0x80, 0x00, 0x01, 0xF8, 0xFF, 0x00, 0x01, 0x08, 0x00, 0x01, 0x01, 0x08, 0x00, 0x01, + 0x01, 0xF8, 0xFF, 0x01, 0x02, 0x60, 0xC0, 0x00, 0x02, 0x20, 0x80, 0x00, 0x04, 0x20, 0x80, 0x00, 0x0C, 0xCE, 0x7F, 0x00, + 0x18, 0x02, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x40, 0x08, 0x00, 0x00, 0xC0, 0x08, 0x00, 0x00, + 0x80, 0x09, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, +}; + +#define question_height 25 +#define question_width 25 +static unsigned char question[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0xE0, 0xFF, 0x07, 0x00, + 0xE0, 0xC3, 0x0F, 0x00, 0xF0, 0x81, 0x0F, 0x00, 0xF0, 0x01, 0x0F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x80, 0x0F, 0x00, + 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x7C, 0x00, 0x00, + 0x00, 0x3C, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +#define bang_height 30 +#define bang_width 30 +static unsigned char bang[] PROGMEM = { + 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x07, 0xF8, 0x3F, 0xFF, 0x07, 0xF8, 0x3F, + 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, + 0xFE, 0x03, 0xF0, 0x1F, 0xFE, 0x03, 0xF0, 0x1F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, + 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x01, 0xE0, 0x0F, 0xFC, 0x01, 0xE0, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0xC0, 0x03, 0xFC, 0x03, 0xF0, 0x0F, 0xFE, 0x03, 0xF0, 0x1F, + 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFC, 0x03, 0xF0, 0x0F, 0xF8, 0x01, 0xE0, 0x07, +}; + +#define haha_height 30 +#define haha_width 30 +static unsigned char haha[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, + 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x1F, 0x3E, 0x00, 0x80, 0x03, 0x70, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0xC0, 0x00, 0xC2, 0x00, + 0x60, 0x00, 0x03, 0x00, 0x60, 0x00, 0xC1, 0x1F, 0x60, 0x80, 0x8F, 0x31, 0x30, 0x0E, 0x80, 0x31, 0x30, 0x10, 0x30, 0x1F, + 0x30, 0x08, 0x58, 0x00, 0x30, 0x04, 0x6C, 0x03, 0x60, 0x00, 0xF3, 0x01, 0x60, 0xC0, 0xFC, 0x01, 0x80, 0x38, 0xBF, 0x01, + 0xE0, 0xC5, 0xDF, 0x00, 0xB0, 0xF9, 0xEF, 0x00, 0x30, 0xF1, 0x73, 0x00, 0xB0, 0x1D, 0x3E, 0x00, 0xF0, 0xFD, 0x0F, 0x00, + 0xE0, 0xE0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +#define wave_icon_height 30 +#define wave_icon_width 30 +static unsigned char wave_icon[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x0C, 0x9C, 0x01, 0x80, 0x17, 0x20, 0x01, 0x80, 0x26, 0x46, 0x02, 0x80, 0x44, 0x88, 0x02, 0xC0, 0x89, 0x8A, 0x02, + 0x40, 0x93, 0x8B, 0x02, 0x40, 0x26, 0x13, 0x00, 0x80, 0x44, 0x16, 0x00, 0xC0, 0x89, 0x24, 0x00, 0x40, 0x93, 0x60, 0x00, + 0x40, 0x26, 0x40, 0x00, 0x80, 0x0C, 0x80, 0x00, 0x00, 0x09, 0x80, 0x00, 0x00, 0x02, 0x80, 0x00, 0x40, 0x06, 0x80, 0x00, + 0x50, 0x0C, 0x80, 0x00, 0x50, 0x08, 0x40, 0x00, 0x90, 0x10, 0x20, 0x00, 0xB0, 0x21, 0x10, 0x00, 0x20, 0x47, 0x18, 0x00, + 0x40, 0x80, 0x0F, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +#define cowboy_height 30 +#define cowboy_width 30 +static unsigned char cowboy[] PROGMEM = { + 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x3C, 0xFE, 0x1F, 0x0F, + 0xFE, 0xFE, 0xDF, 0x1F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, + 0x3E, 0xC0, 0x00, 0x1F, 0x1E, 0x00, 0x00, 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x08, 0x0E, 0x1C, 0x04, 0x00, 0x0E, 0x1C, 0x00, + 0x04, 0x0E, 0x1C, 0x08, 0x04, 0x0E, 0x1C, 0x08, 0x04, 0x04, 0x08, 0x08, 0x04, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0x08, + 0x8C, 0x07, 0x70, 0x0C, 0x88, 0xFC, 0x4F, 0x04, 0x88, 0x01, 0x40, 0x04, 0x90, 0xFF, 0x7F, 0x02, 0x30, 0x03, 0x30, 0x03, + 0x60, 0x0E, 0x9C, 0x01, 0xC0, 0xF8, 0xC7, 0x00, 0x80, 0x01, 0x60, 0x00, 0x00, 0x0E, 0x1C, 0x00, 0x00, 0xF8, 0x07, 0x00, +}; + +#define deadmau5_height 30 +#define deadmau5_width 60 +static unsigned char deadmau5[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x07, 0x00, + 0x00, 0xFC, 0x03, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0x3F, 0x00, + 0xE0, 0xFF, 0xFF, 0x01, 0xF0, 0xFF, 0x7F, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0xF8, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x07, + 0xFC, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x0F, 0xFE, 0xFF, 0xFF, 0x00, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0x3F, 0xFC, + 0x0F, 0xFF, 0x7F, 0x00, 0xC0, 0xFF, 0x1F, 0xF8, 0x0F, 0xFC, 0x3F, 0x00, 0x80, 0xFF, 0x0F, 0xF8, 0x1F, 0xFC, 0x1F, 0x00, + 0x00, 0xFF, 0x0F, 0xFC, 0x3F, 0xFC, 0x0F, 0x00, 0x00, 0xF8, 0x1F, 0xFF, 0xFF, 0xFE, 0x01, 0x00, 0x00, 0x00, 0xFC, 0xFF, + 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x80, 0x07, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +#define sun_width 30 +#define sun_height 30 +static unsigned char sun[] PROGMEM = { + 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x30, 0xC0, 0x00, 0x03, + 0x70, 0x00, 0x80, 0x03, 0xF0, 0x00, 0xC0, 0x03, 0xF0, 0xF8, 0xC7, 0x03, 0xE0, 0xFC, 0xCF, 0x01, 0x00, 0xFE, 0x1F, 0x00, + 0x00, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x8E, 0xFF, 0x7F, 0x1C, 0x9F, 0xFF, 0x7F, 0x3E, + 0x9F, 0xFF, 0x7F, 0x3E, 0x8E, 0xFF, 0x7F, 0x1C, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0xFF, 0x3F, 0x00, + 0x00, 0xFE, 0x1F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0xC0, 0xF9, 0xE7, 0x00, 0xE0, 0x01, 0xE0, 0x01, 0xF0, 0x01, 0xE0, 0x03, + 0xF0, 0xC0, 0xC0, 0x03, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, +}; + +#define rain_width 30 +#define rain_height 30 +static unsigned char rain[] PROGMEM = { + 0xC0, 0x0F, 0xC0, 0x00, 0x40, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x03, 0x38, 0x00, + 0x00, 0x0E, 0x0C, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x10, 0x03, 0x00, 0x00, 0x30, 0x01, 0x00, 0x00, 0x20, + 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x03, 0x00, 0x00, 0x30, 0x02, 0x00, + 0x00, 0x10, 0x06, 0x00, 0x00, 0x08, 0xFC, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x01, 0x80, 0x00, 0x01, 0x00, + 0xC0, 0xC0, 0x81, 0x03, 0xA0, 0x60, 0xC1, 0x03, 0x90, 0x20, 0x41, 0x01, 0xF0, 0xE0, 0xC0, 0x01, 0x60, 0x4C, + 0x98, 0x00, 0x00, 0x0E, 0x1C, 0x00, 0x00, 0x0B, 0x12, 0x00, 0x00, 0x09, 0x1A, 0x00, 0x00, 0x06, 0x0E, 0x00, +}; + +#define cloud_height 30 +#define cloud_width 30 +static unsigned char cloud[] PROGMEM = { + 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x70, 0x30, 0x00, 0x00, 0x10, 0x60, 0x00, 0x80, 0x1F, 0x40, 0x00, + 0xC0, 0x0F, 0xC0, 0x00, 0xC0, 0x00, 0x80, 0x00, 0x60, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x01, + 0x20, 0x00, 0x00, 0x07, 0x38, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x08, 0x06, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x10, + 0x02, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, + 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x10, + 0x02, 0x00, 0x00, 0x10, 0x06, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0xFC, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x03, +}; + +#define fog_height 25 +#define fog_width 25 +static unsigned char fog[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x3C, 0x00, 0xFE, 0x01, 0xFF, 0x00, 0x87, 0xC7, 0xC3, 0x01, 0x03, 0xFE, 0x80, 0x01, + 0x00, 0x38, 0x00, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0xFF, 0x83, 0xFF, 0x01, 0x03, 0xFF, 0x81, 0x01, 0x00, 0x7C, 0x00, 0x00, + 0xF8, 0x00, 0x3E, 0x00, 0xFE, 0x01, 0xFF, 0x00, 0x87, 0xC7, 0xC3, 0x01, 0x03, 0xFE, 0x80, 0x01, 0x00, 0x38, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +#define devil_height 30 +#define devil_width 30 +static unsigned char devil[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x10, 0x03, 0xC0, 0x01, 0x38, 0x07, 0x7C, 0x0F, 0x38, 0x1F, 0x03, 0x30, 0x1E, + 0xFE, 0x01, 0xE0, 0x1F, 0x7E, 0x00, 0x80, 0x1F, 0x3C, 0x00, 0x00, 0x0F, 0x1C, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x06, + 0x08, 0x00, 0x00, 0x04, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x0E, 0x1C, 0x0C, + 0x0C, 0x18, 0x06, 0x0C, 0x0C, 0x1C, 0x06, 0x0C, 0x0C, 0x1C, 0x0E, 0x0C, 0x0C, 0x1C, 0x0E, 0x0C, 0x0C, 0x0C, 0x06, 0x0C, + 0x08, 0x00, 0x00, 0x06, 0x18, 0x02, 0x10, 0x06, 0x10, 0x0C, 0x0C, 0x03, 0x30, 0xF8, 0x07, 0x03, 0x60, 0xE0, 0x80, 0x01, + 0xC0, 0x00, 0xC0, 0x00, 0x80, 0x01, 0x70, 0x00, 0x00, 0x06, 0x1C, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +#define heart_height 30 +#define heart_width 30 +static unsigned char heart[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0xF0, 0x00, 0xF8, 0x0F, 0xFC, 0x07, 0xFC, 0x1F, 0x06, 0x0E, 0xFE, 0x3F, 0x03, 0x18, + 0xFE, 0xFF, 0x7F, 0x10, 0xFF, 0xFF, 0xFF, 0x31, 0xFF, 0xFF, 0xFF, 0x33, 0xFF, 0xFF, 0xFF, 0x37, 0xFF, 0xFF, 0xFF, 0x37, + 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFE, 0xFF, 0xFF, 0x1F, 0xFE, 0xFF, 0xFF, 0x1F, + 0xFC, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0x0F, 0xF8, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x03, 0xF0, 0xFF, 0xFF, 0x03, + 0xE0, 0xFF, 0xFF, 0x01, 0xC0, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFE, 0x1F, 0x00, + 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x00, 0x00, +}; + +#define poo_width 30 +#define poo_height 30 +static unsigned char poo[] PROGMEM = { + 0x00, 0x1C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xEC, 0x01, 0x00, 0x00, 0x8C, 0x07, 0x00, 0x00, 0x0C, 0x06, 0x00, + 0x00, 0x24, 0x0C, 0x00, 0x00, 0x34, 0x08, 0x00, 0x00, 0x1F, 0x08, 0x00, 0xC0, 0x0F, 0x08, 0x00, 0xC0, 0x00, 0x3C, 0x00, + 0x60, 0x00, 0x7C, 0x00, 0x60, 0x00, 0xC6, 0x00, 0x20, 0x00, 0xCB, 0x00, 0xA0, 0xC7, 0xFF, 0x00, 0xE0, 0x7F, 0xF7, 0x00, + 0xF0, 0x18, 0xE3, 0x03, 0x78, 0x18, 0x41, 0x03, 0x6C, 0x9B, 0x5D, 0x06, 0x64, 0x9B, 0x5D, 0x04, 0x44, 0x1A, 0x41, 0x04, + 0x4C, 0xD8, 0x63, 0x06, 0xF8, 0xFC, 0x36, 0x06, 0xFE, 0x0F, 0x9C, 0x1F, 0x07, 0x03, 0xC0, 0x30, 0x03, 0x00, 0x78, 0x20, + 0x01, 0x00, 0x1F, 0x20, 0x03, 0xE0, 0x03, 0x20, 0x07, 0x7E, 0x04, 0x30, 0xFE, 0x0F, 0xFC, 0x1F, 0xF0, 0x00, 0xF0, 0x0F, +}; +#endif + +#include "img/icon.xbm" diff --git a/src/graphics/img/icon.xbm b/src/graphics/img/icon.xbm new file mode 100644 index 0000000..4e78ae8 --- /dev/null +++ b/src/graphics/img/icon.xbm @@ -0,0 +1,22 @@ +#ifndef USERPREFS_HAS_SPLASH +#define icon_width 50 +#define icon_height 28 +static uint8_t icon_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x03, + 0x00, 0x00, 0x00, 0x80, 0x07, 0xC0, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x1F, + 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0xE0, 0x0F, 0x00, 0x00, 0x00, + 0xE0, 0x0F, 0xF0, 0x1F, 0x00, 0x00, 0x00, 0xF0, 0x07, 0xF0, 0x3F, 0x00, + 0x00, 0x00, 0xF8, 0x03, 0xF8, 0x7F, 0x00, 0x00, 0x00, 0xF8, 0x03, 0xFC, + 0x7F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0xFC, 0xFE, 0x00, 0x00, 0x00, 0xFE, + 0x00, 0xFE, 0xFC, 0x01, 0x00, 0x00, 0xFE, 0x00, 0x7F, 0xFC, 0x01, 0x00, + 0x00, 0x7F, 0x00, 0x3F, 0xF8, 0x03, 0x00, 0x80, 0x3F, 0x80, 0x3F, 0xF0, + 0x07, 0x00, 0x80, 0x3F, 0xC0, 0x1F, 0xF0, 0x07, 0x00, 0xC0, 0x1F, 0xC0, + 0x0F, 0xE0, 0x0F, 0x00, 0xE0, 0x0F, 0xE0, 0x0F, 0xC0, 0x1F, 0x00, 0xE0, + 0x0F, 0xF0, 0x07, 0x80, 0x1F, 0x00, 0xF0, 0x07, 0xF8, 0x03, 0x80, 0x3F, + 0x00, 0xF8, 0x03, 0xF8, 0x03, 0x00, 0x7F, 0x00, 0xFC, 0x03, 0xFC, 0x01, + 0x00, 0x7E, 0x00, 0xFC, 0x01, 0xFE, 0x00, 0x00, 0xFE, 0x00, 0xFE, 0x00, + 0xFE, 0x00, 0x00, 0xFC, 0x01, 0x7E, 0x00, 0x7F, 0x00, 0x00, 0xF8, 0x01, + 0x7E, 0x00, 0x3E, 0x00, 0x00, 0xF8, 0x01, 0x38, 0x00, 0x3C, 0x00, 0x00, + 0x70, 0x00, 0x10, 0x00, 0x10, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, }; +#endif \ No newline at end of file diff --git a/src/input/BBQ10Keyboard.cpp b/src/input/BBQ10Keyboard.cpp new file mode 100644 index 0000000..8f8399a --- /dev/null +++ b/src/input/BBQ10Keyboard.cpp @@ -0,0 +1,181 @@ +// Based on arturo182 arduino_bbq10kbd library https://github.com/arturo182/arduino_bbq10kbd + +#include + +#include "BBQ10Keyboard.h" + +#define _REG_VER 1 +#define _REG_CFG 2 +#define _REG_INT 3 +#define _REG_KEY 4 +#define _REG_BKL 5 +#define _REG_DEB 6 +#define _REG_FRQ 7 +#define _REG_RST 8 +#define _REG_FIF 9 + +#define _WRITE_MASK (1 << 7) + +#define CFG_OVERFLOW_ON (1 << 0) +#define CFG_OVERFLOW_INT (1 << 1) +#define CFG_CAPSLOCK_INT (1 << 2) +#define CFG_NUMLOCK_INT (1 << 3) +#define CFG_KEY_INT (1 << 4) +#define CFG_PANIC_INT (1 << 5) +#define CFG_REPORT_MODS (1 << 6) +#define CFG_USE_MODS (1 << 7) + +#define INT_OVERFLOW (1 << 0) +#define INT_CAPSLOCK (1 << 1) +#define INT_NUMLOCK (1 << 2) +#define INT_KEY (1 << 3) +#define INT_PANIC (1 << 4) + +#define KEY_CAPSLOCK (1 << 5) +#define KEY_NUMLOCK (1 << 6) +#define KEY_COUNT_MASK (0x1F) + +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; + + m_wire->begin(); + + 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::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::clearInterruptStatus() +{ + writeRegister(_REG_INT, 0x00); +} + +uint8_t BBQ10Keyboard::status() const +{ + return readRegister8(_REG_KEY); +} + +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); + + 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; +} + +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 new file mode 100644 index 0000000..07d0230 --- /dev/null +++ b/src/input/BBQ10Keyboard.h @@ -0,0 +1,51 @@ +// Based on arturo182 arduino_bbq10kbd library https://github.com/arturo182/arduino_bbq10kbd + +#include "configuration.h" +#include + +#define KEY_MOD_ALT (0x1A) +#define KEY_MOD_SHL (0x1B) +#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); + + enum KeyState { StateIdle = 0, StatePress, StateLongPress, StateRelease }; + + struct KeyEvent { + char key; + KeyState state; + }; + + BBQ10Keyboard(); + + 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 reset(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; + + 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); + + private: + TwoWire *m_wire; + uint8_t m_addr; + i2c_com_fptr_t readCallback; + i2c_com_fptr_t writeCallback; +}; diff --git a/src/input/ExpressLRSFiveWay.cpp b/src/input/ExpressLRSFiveWay.cpp new file mode 100644 index 0000000..56413bd --- /dev/null +++ b/src/input/ExpressLRSFiveWay.cpp @@ -0,0 +1,259 @@ +#include "ExpressLRSFiveWay.h" +#include "Throttle.h" + +#ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE + +static const char inputSourceName[] = "ExpressLRS5Way"; // should match "allow input source" string + +/** + * @brief Calculate fuzz: half the distance to the next nearest neighbor for each joystick position. + * + * The goal is to avoid collisions between joystick positions while still maintaining + * the widest tolerance for the analog value. + * + * Example: {10,50,800,1000,300,1600} + * If we just choose the minimum difference for this array the value would + * be 40/2 = 20. + * + * 20 does not leave enough room for the joystick position using 1600 which + * could have a +-100 offset. + * + * 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 + + // 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); + + 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; + + // Express LRS: calculate the threshold for interpreting ADC values as various buttons + calcFuzzValues(); + + // Meshtastic: register with canned messages + inputBroker->registerSource(this); +} + +// ExpressLRS: interpret reading as key events +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; + } + } + 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; +} + +// Meshtastic: runs at regular intervals +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(); + } + + // 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); + + // 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; +} + +// 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(CANCEL); // exit the menu (press imaginary cancel key) + else + sendKey(LEFT); + break; + + case RIGHT: + if (inCannedMessageMenu()) // If in canned message menu: + sendKey(CANCEL); // exit the menu (press imaginary cancel key) + else + sendKey(RIGHT); + break; + + case UP: + if (length == LONG) + toggleGPS(); + else + sendKey(UP); + break; + + case DOWN: + if (length == LONG) + sendAdhocPing(); + else + sendKey(DOWN); + break; + + case OK: + if (length == LONG) + shutdown(); + else + click(); // Use instead of sendKey(OK). Works better when canned message module disabled + break; + + default: + break; + } +} + +// Feed input to the canned messages module +void ExpressLRSFiveWay::sendKey(KeyType 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() +{ +#if HAS_GPS && !MESHTASTIC_EXCLUDE_GPS + if (!config.device.disable_triple_click && (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); + + // 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(); +} + +// Shutdown the node (enter deep-sleep) +// Contained as one method for easier remapping of buttons by user +void ExpressLRSFiveWay::shutdown() +{ + LOG_INFO("Shutdown from long press"); + powerFSM.trigger(EVENT_PRESS); + screen->startAlert("Shutting down..."); + // Don't set alerting = true. We don't want to auto-dismiss this alert. + + playShutdownMelody(); // In case user adds a buzzer + + shutdownAtMsec = millis() + 3000; +} + +// Emulate user button, or canned message SELECT +// This is necessary as canned message module doesn't translate SELECT to user button presses if the module is disabled +// Contained as one method for easier remapping of buttons by user +void ExpressLRSFiveWay::click() +{ + if (!moduleConfig.canned_message.enabled) + powerFSM.trigger(EVENT_PRESS); + else + sendKey(OK); +} + +ExpressLRSFiveWay *expressLRSFiveWayInput = nullptr; + +#endif \ No newline at end of file diff --git a/src/input/ExpressLRSFiveWay.h b/src/input/ExpressLRSFiveWay.h new file mode 100644 index 0000000..c53aa9c --- /dev/null +++ b/src/input/ExpressLRSFiveWay.h @@ -0,0 +1,85 @@ +/* + Input source for Radio Master Bandit Nano, and similar hardware. + Devices have a 5-button "resistor ladder" style joystick, read by ADC. + These devices do not use the ADC to monitor input voltage. + + Much of this code taken directly from ExpressLRS FiveWayButton class: + https://github.com/ExpressLRS/ExpressLRS/tree/d9f56f8bd6f9f7144d5f01caaca766383e1e0950/src/lib/SCREEN/FiveWayButton +*/ + +#pragma once + +#include "configuration.h" + +#ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE + +#include +#include + +#include "InputBroker.h" +#include "MeshService.h" // For adhoc ping action +#include "buzz.h" +#include "concurrency/OSThread.h" +#include "graphics/Screen.h" // Feedback for adhoc ping / toggle GPS +#include "main.h" +#include "modules/CannedMessageModule.h" + +#if HAS_GPS && !MESHTASTIC_EXCLUDE_GPS +#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 + + // 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 = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP, + DOWN = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN, + LEFT = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT, + RIGHT = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT, + OK = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT, + CANCEL = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL, + NO_PRESS = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE + } KeyType; + + 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); + + // Meshtastic code + void determineAction(KeyType key, PressLength length); + void sendKey(KeyType 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(); + + 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(); +}; + +extern ExpressLRSFiveWay *expressLRSFiveWayInput; + +#endif \ No newline at end of file diff --git a/src/input/InputBroker.cpp b/src/input/InputBroker.cpp new file mode 100644 index 0000000..cb73e32 --- /dev/null +++ b/src/input/InputBroker.cpp @@ -0,0 +1,18 @@ +#include "InputBroker.h" +#include "PowerFSM.h" // needed for event trigger + +InputBroker *inputBroker = nullptr; + +InputBroker::InputBroker(){}; + +void InputBroker::registerSource(Observable *source) +{ + this->inputEventObserver.observe(source); +} + +int InputBroker::handleInputEvent(const InputEvent *event) +{ + powerFSM.trigger(EVENT_INPUT); + this->notifyObservers(event); + return 0; +} \ No newline at end of file diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h new file mode 100644 index 0000000..db7524b --- /dev/null +++ b/src/input/InputBroker.h @@ -0,0 +1,43 @@ +#pragma once +#include "Observer.h" + +#define ANYKEY 0xFF +#define MATRIXKEY 0xFE + +#define INPUT_BROKER_MSG_BRIGHTNESS_UP 0x11 +#define INPUT_BROKER_MSG_BRIGHTNESS_DOWN 0x12 +#define INPUT_BROKER_MSG_REBOOT 0x90 +#define INPUT_BROKER_MSG_SHUTDOWN 0x9b +#define INPUT_BROKER_MSG_GPS_TOGGLE 0x9e +#define INPUT_BROKER_MSG_MUTE_TOGGLE 0xac +#define INPUT_BROKER_MSG_SEND_PING 0xaf +#define INPUT_BROKER_MSG_DISMISS_FRAME 0x8b +#define INPUT_BROKER_MSG_LEFT 0xb4 +#define INPUT_BROKER_MSG_UP 0xb5 +#define INPUT_BROKER_MSG_DOWN 0xb6 +#define INPUT_BROKER_MSG_RIGHT 0xb7 +#define INPUT_BROKER_MSG_FN_SYMBOL_ON 0xf1 +#define INPUT_BROKER_MSG_FN_SYMBOL_OFF 0xf2 +#define INPUT_BROKER_MSG_BLUETOOTH_TOGGLE 0xAA + +typedef struct _InputEvent { + const char *source; + char inputEvent; + char kbchar; + uint16_t touchX; + uint16_t touchY; +} InputEvent; +class InputBroker : public Observable +{ + CallbackObserver inputEventObserver = + CallbackObserver(this, &InputBroker::handleInputEvent); + + public: + InputBroker(); + void registerSource(Observable *source); + + protected: + int handleInputEvent(const InputEvent *event); +}; + +extern InputBroker *inputBroker; \ No newline at end of file diff --git a/src/input/LinuxInput.cpp b/src/input/LinuxInput.cpp new file mode 100644 index 0000000..57a87b0 --- /dev/null +++ b/src/input/LinuxInput.cpp @@ -0,0 +1,187 @@ +#include "configuration.h" +#if ARCH_PORTDUINO +#include "LinuxInput.h" +#include "platform/portduino/PortduinoGlue.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// 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); +} + +int32_t LinuxInput::runOnce() +{ + + if (firstTime) { + if (settingsStrings[keyboardDevice] == "") + return disable(); + fd = open(settingsStrings[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); + + ev.events = EPOLLIN; + ev.data.fd = fd; + if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev)) { + perror("unable to epoll add"); + return disable(); + } + // This is the first time the OSThread library has called this function, so do port setup + firstTime = 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 = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_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; + } + 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 = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL; + break; + case KEY_BACK: // Back + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK; + // e.kbchar = key; + break; + + case KEY_UP: // Up + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP; + break; + case KEY_DOWN: // Down + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN; + break; + case KEY_LEFT: // Left + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT; + break; + e.kbchar = INPUT_BROKER_MSG_LEFT; + case KEY_RIGHT: // Right + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT; + break; + e.kbchar = INPUT_BROKER_MSG_RIGHT; + case KEY_ENTER: // Enter + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; + break; + case KEY_POWER: + system("poweroff"); + break; + default: // all other keys + if (keymap[code]) { + e.inputEvent = ANYKEY; + e.kbchar = keymap[code]; + } + break; + } + } + if (ev[j].value) { + modifiers |= mod; + } else { + modifiers &= ~mod; + } + report[0] = modifiers; + } + if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + if (e.inputEvent == 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 new file mode 100644 index 0000000..43d0849 --- /dev/null +++ b/src/input/LinuxInput.h @@ -0,0 +1,65 @@ +#pragma once +#if ARCH_PORTDUINO +#include "InputBroker.h" +#include "concurrency/OSThread.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 + + protected: + virtual int32_t runOnce() override; + + 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; + + 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 new file mode 100644 index 0000000..4ddda19 --- /dev/null +++ b/src/input/LinuxInputImpl.cpp @@ -0,0 +1,15 @@ +#include "configuration.h" +#if ARCH_PORTDUINO +#include "InputBroker.h" +#include "LinuxInputImpl.h" + +LinuxInputImpl *aLinuxInputImpl; + +LinuxInputImpl::LinuxInputImpl() : LinuxInput("LinuxInput") {} + +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 new file mode 100644 index 0000000..e734b02 --- /dev/null +++ b/src/input/LinuxInputImpl.h @@ -0,0 +1,21 @@ +#ifdef ARCH_PORTDUINO +#pragma once +#include "LinuxInput.h" +#include "main.h" + +/** + * @brief The idea behind this class to have static methods for the event handlers. + * Check attachInterrupt() at RotaryEncoderInteruptBase.cpp + * Technically you can have as many rotary encoders hardver attached + * to your device as you wish, but you always need to have separate event + * handlers, thus you need to have a RotaryEncoderInterrupt implementation. + */ + +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 new file mode 100644 index 0000000..e1b32aa --- /dev/null +++ b/src/input/MPR121Keyboard.cpp @@ -0,0 +1,430 @@ +// Based on the BBQ10 Keyboard + +#include "MPR121Keyboard.h" +#include "configuration.h" +#include + +#define _MPR121_REG_KEY 0x5a + +#define _MPR121_REG_TOUCH_STATUS 0x00 +#define _MPR121_REG_ELECTRODE_FILTERED_DATA +#define _MPR121_REG_BASELINE_VALUE 0x1E + +// Baseline filters +#define _MPR121_REG_MAX_HALF_DELTA_RISING 0x2B +#define _MPR121_REG_NOISE_HALF_DELTA_RISING 0x2C +#define _MPR121_REG_NOISE_COUNT_LIMIT_RISING 0x2D +#define _MPR121_REG_FILTER_DELAY_COUNT_RISING 0x2E +#define _MPR121_REG_MAX_HALF_DELTA_FALLING 0x2F +#define _MPR121_REG_NOISE_HALF_DELTA_FALLING 0x30 +#define _MPR121_REG_NOISE_COUNT_LIMIT_FALLING 0x31 +#define _MPR121_REG_FILTER_DELAY_COUNT_FALLING 0x32 +#define _MPR121_REG_NOISE_HALF_DELTA_TOUCHED 0x33 +#define _MPR121_REG_NOISE_COUNT_LIMIT_TOUCHED 0x34 +#define _MPR121_REG_FILTER_DELAY_COUNT_TOUCHED 0x35 + +#define _MPR121_REG_TOUCH_THRESHOLD 0x41 // First input, +2 for subsequent +#define _MPR121_REG_RELEASE_THRESHOLD 0x42 // First input, +2 for subsequent +#define _MPR121_REG_DEBOUNCE 0x5B +#define _MPR121_REG_CONFIG1 0x5C +#define _MPR121_REG_CONFIG2 0x5D +#define _MPR121_REG_ELECTRODE_CONFIG 0x5E +#define _MPR121_REG_SOFT_RESET 0x80 + +#define _KEY_MASK 0x0FFF // Key mask for the first 12 bits +#define _NUM_KEYS 12 + +#define ECR_CALIBRATION_TRACK_FROM_ZERO (0 << 6) +#define ECR_CALIBRATION_LOCK (1 << 6) +#define ECR_CALIBRATION_TRACK_FROM_PARTIAL_FILTER (2 << 6) // Recommended Typical Mode +#define ECR_CALIBRATION_TRACK_FROM_FULL_FILTER (3 << 6) +#define ECR_PROXIMITY_DETECTION_OFF (0 << 0) // Not using proximity detection +#define ECR_TOUCH_DETECTION_12CH (12 << 0) // Using all 12 channels + +#define MPR121_NONE 0x00 +#define MPR121_REBOOT 0x90 +#define MPR121_LEFT 0xb4 +#define MPR121_UP 0xb5 +#define MPR121_DOWN 0xb6 +#define MPR121_RIGHT 0xb7 +#define MPR121_ESC 0x1b +#define MPR121_BSP 0x08 +#define MPR121_SELECT 0x0d + +#define MPR121_FN_ON 0xf1 +#define MPR121_FN_OFF 0xf2 + +#define LONG_PRESS_THRESHOLD 2000 +#define MULTI_TAP_THRESHOLD 2000 + +uint8_t TapMod[12] = {1, 2, 1, 13, 7, 7, 7, 7, 7, 9, 7, 9}; // Num chars per key, Modulus for rotating through characters + +unsigned char MPR121_TapMap[12][13] = {{MPR121_BSP}, + {'0', ' '}, + {MPR121_SELECT}, + {'1', '.', ',', '?', '!', ':', ';', '-', '_', '\\', '/', '(', ')'}, + {'2', 'a', 'b', 'c', 'A', 'B', 'C'}, + {'3', 'd', 'e', 'f', 'D', 'E', 'F'}, + {'4', 'g', 'h', 'i', 'G', 'H', 'I'}, + {'5', 'j', 'k', 'l', 'J', 'K', 'L'}, + {'6', 'm', 'n', 'o', 'M', 'N', 'O'}, + {'7', 'p', 'q', 'r', 's', 'P', 'Q', 'R', 'S'}, + {'8', 't', 'u', 'v', 'T', 'U', 'V'}, + {'9', 'w', 'x', 'y', 'z', 'W', 'X', 'Y', 'Z'}}; + +unsigned char MPR121_LongPressMap[12] = {MPR121_ESC, ' ', MPR121_NONE, MPR121_NONE, MPR121_UP, MPR121_NONE, + MPR121_LEFT, MPR121_NONE, MPR121_RIGHT, MPR121_NONE, MPR121_DOWN, MPR121_NONE}; + +// Translation map from left to right, top to bottom layout to a more convenient layout to manufacture, matching the +// https://www.amazon.com.au/Capacitive-Sensitive-Sensitivity-Replacement-Traditional/dp/B0CTJD5KW9/ref=pd_ci_mcx_mh_mcx_views_0_title?th=1 +/*uint8_t MPR121_KeyMap[12] = { + 9, 6, 3, 0, + 10, 7, 4, 1, + 11, 8, 5, 2 +};*/ +// 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\n", 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; + + m_wire->begin(); + + 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 Resetting..."); + // 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), 15); + delay(20); + // Set release threshold + writeRegister(_MPR121_REG_RELEASE_THRESHOLD + (i * 2), 7); + delay(20); + } + // Configure filtering and baseline registers + writeRegister(_MPR121_REG_MAX_HALF_DELTA_RISING, 0x01); + 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, 0x0e); + 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); + // Set Debounce to 0x02 + writeRegister(_MPR121_REG_DEBOUNCE, 0x00); + delay(20); + // Set Filter1 itterations and discharge current 6x and 16uA respectively (0x10) + writeRegister(_MPR121_REG_CONFIG1, 0x10); + delay(20); + // Set CDT to 0.5us, Filter2 itterations to 4x, and Sample interval = 0 (0x20) + writeRegister(_MPR121_REG_CONFIG2, 0x20); + 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_PARTIAL_FILTER | ECR_PROXIMITY_DETECTION_OFF | ECR_TOUCH_DETECTION_12CH); + delay(100); + LOG_DEBUG("MPR121 Running"); + 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++; + } + } + + return numButtonsPressed; +} + +bool MPR121Keyboard::hasEvent() +{ + return queue.length() > 0; +} + +void MPR121Keyboard::queueEvent(char next) +{ + if (next == MPR121_NONE) { + return; + } + 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; + 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]); + } + return; +} + +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; +} + +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)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); + } +} diff --git a/src/input/MPR121Keyboard.h b/src/input/MPR121Keyboard.h new file mode 100644 index 0000000..6349750 --- /dev/null +++ b/src/input/MPR121Keyboard.h @@ -0,0 +1,56 @@ +// Based on the BBQ10 Keyboard + +#include "concurrency/NotifiedWorkerThread.h" +#include "configuration.h" +#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); + + enum MPR121States { Init = 0, Idle, Held, HeldLong, Busy }; + + MPR121States state; + + int8_t last_key; + uint32_t last_tap; + uint8_t char_idx; + + String queue; + + MPR121Keyboard(); + + 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 reset(void); + + 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); + + 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); + + 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; +}; \ No newline at end of file diff --git a/src/input/RotaryEncoderInterruptBase.cpp b/src/input/RotaryEncoderInterruptBase.cpp new file mode 100644 index 0000000..785d98e --- /dev/null +++ b/src/input/RotaryEncoderInterruptBase.cpp @@ -0,0 +1,116 @@ +#include "RotaryEncoderInterruptBase.h" +#include "configuration.h" + +RotaryEncoderInterruptBase::RotaryEncoderInterruptBase(const char *name) : concurrency::OSThread(name) +{ + this->_originName = name; +} + +void RotaryEncoderInterruptBase::init( + uint8_t pinA, uint8_t pinB, uint8_t pinPress, char eventCw, char eventCcw, char eventPressed, + // std::function onIntA, std::function onIntB, std::function onIntPress) : + void (*onIntA)(), void (*onIntB)(), void (*onIntPress)()) +{ + this->_pinA = pinA; + this->_pinB = pinB; + this->_eventCw = eventCw; + this->_eventCcw = eventCcw; + this->_eventPressed = eventPressed; + + pinMode(pinPress, INPUT_PULLUP); + pinMode(this->_pinA, INPUT_PULLUP); + pinMode(this->_pinB, INPUT_PULLUP); + + // attachInterrupt(pinPress, onIntPress, RISING); + attachInterrupt(pinPress, onIntPress, RISING); + attachInterrupt(this->_pinA, onIntA, CHANGE); + 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 = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.source = this->_originName; + + if (this->action == ROTARY_ACTION_PRESSED) { + LOG_DEBUG("Rotary event Press"); + e.inputEvent = this->_eventPressed; + } 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 != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + this->notifyObservers(&e); + } + + this->action = ROTARY_ACTION_NONE; + + return INT32_MAX; +} + +void RotaryEncoderInterruptBase::intPressHandler() +{ + this->action = ROTARY_ACTION_PRESSED; + setIntervalFromNow(20); // TODO: this modifies a non-volatile variable! +} + +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); +} + +/** + * @brief Rotary action implementation. + * We assume, the following pin setup: + * A --|| + * GND --||]======== + * B --|| + * + * @return The new state for rotary pin. + */ +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; + } + setIntervalFromNow(50); // TODO: this modifies a non-volatile variable! + + return newState; +} diff --git a/src/input/RotaryEncoderInterruptBase.h b/src/input/RotaryEncoderInterruptBase.h new file mode 100644 index 0000000..9bcf25a --- /dev/null +++ b/src/input/RotaryEncoderInterruptBase.h @@ -0,0 +1,41 @@ +#pragma once + +#include "InputBroker.h" +#include "concurrency/OSThread.h" +#include "mesh/NodeDB.h" + +enum RotaryEncoderInterruptBaseStateType { ROTARY_EVENT_OCCURRED, ROTARY_EVENT_CLEARED }; + +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, char eventCw, char eventCcw, char eventPressed, + // 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); + + 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: + uint8_t _pinA = 0; + uint8_t _pinB = 0; + char _eventCw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + char _eventCcw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + char _eventPressed = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + const char *_originName; +}; diff --git a/src/input/RotaryEncoderInterruptImpl1.cpp b/src/input/RotaryEncoderInterruptImpl1.cpp new file mode 100644 index 0000000..7e79289 --- /dev/null +++ b/src/input/RotaryEncoderInterruptImpl1.cpp @@ -0,0 +1,42 @@ +#include "RotaryEncoderInterruptImpl1.h" +#include "InputBroker.h" + +RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1; + +RotaryEncoderInterruptImpl1::RotaryEncoderInterruptImpl1() : RotaryEncoderInterruptBase("rotEnc1") {} + +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; + char eventCw = static_cast(moduleConfig.canned_message.inputbroker_event_cw); + char eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw); + char eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press); + + // moduleConfig.canned_message.ext_notification_module_output + RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed, + RotaryEncoderInterruptImpl1::handleIntA, RotaryEncoderInterruptImpl1::handleIntB, + RotaryEncoderInterruptImpl1::handleIntPressed); + inputBroker->registerSource(this); + return true; +} + +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 new file mode 100644 index 0000000..22ecba3 --- /dev/null +++ b/src/input/RotaryEncoderInterruptImpl1.h @@ -0,0 +1,21 @@ +#pragma once +#include "RotaryEncoderInterruptBase.h" + +/** + * @brief The idea behind this class to have static methods for the event handlers. + * Check attachInterrupt() at RotaryEncoderInteruptBase.cpp + * Technically you can have as many rotary encoders hardver attached + * 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(); +}; + +extern RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1; \ No newline at end of file diff --git a/src/input/ScanAndSelect.cpp b/src/input/ScanAndSelect.cpp new file mode 100644 index 0000000..e1b39ed --- /dev/null +++ b/src/input/ScanAndSelect.cpp @@ -0,0 +1,205 @@ +#include "configuration.h" + +// Normally these input methods are protected by guarding in setupModules +// In order to have the user button dismiss the canned message frame, this class lightly interacts with the Screen class +#if HAS_SCREEN + +#include "ScanAndSelect.h" +#include "modules/CannedMessageModule.h" +#include + +// Config +static const char name[] = "scanAndSelect"; // should match "allow input source" string +static constexpr uint32_t durationShortMs = 50; +static constexpr uint32_t durationLongMs = 1500; +static constexpr uint32_t durationAlertMs = 2000; + +// Constructor: init base class +ScanAndSelectInput::ScanAndSelectInput() : concurrency::OSThread(name) {} + +// Attempt to setup class; true if success. +// Called by setupModules method. Instance deleted if setup fails. +bool ScanAndSelectInput::init() +{ + // Short circuit: Canned messages enabled? + if (!moduleConfig.canned_message.enabled) + return false; + + // Short circuit: Using correct "input source"? + // Todo: protobuf enum instead of string? + if (strcasecmp(moduleConfig.canned_message.allow_input_source, name) != 0) + return false; + + // Use any available inputbroker pin as the button + if (moduleConfig.canned_message.inputbroker_pin_press) + pin = moduleConfig.canned_message.inputbroker_pin_press; + else if (moduleConfig.canned_message.inputbroker_pin_a) + pin = moduleConfig.canned_message.inputbroker_pin_a; + else if (moduleConfig.canned_message.inputbroker_pin_b) + pin = moduleConfig.canned_message.inputbroker_pin_b; + else + return false; // Short circuit: no button found + + // Set-up the button + pinMode(pin, INPUT_PULLUP); + attachInterrupt(pin, handleChangeInterrupt, CHANGE); + + // Connect our class to the canned message module + inputBroker->registerSource(this); + + LOG_INFO("Initialized 'Scan and Select' input for Canned Messages, using pin %d", pin); + return true; // Init succeded +} + +// Runs periodically, unless sleeping between presses +int32_t ScanAndSelectInput::runOnce() +{ + uint32_t now = millis(); + + // If: "no messages added" alert screen currently shown + if (alertingNoMessage) { + // Dismiss the alert screen several seconds after it appears + if (now > alertingSinceMs + durationAlertMs) { + alertingNoMessage = false; + screen->endAlert(); + } + } + + // If: Button is pressed + if (digitalRead(pin) == LOW) { + // New press + if (!held) { + downSinceMs = now; + } + + // Existing press + else { + // Duration enough for long press + // Long press not yet fired (prevent repeat firing while held) + if (!longPressFired && Throttle::isWithinTimespanMs(downSinceMs, durationLongMs)) { + longPressFired = true; + longPress(); + } + } + + // Record the change of state: button is down + held = true; + } + + // If: Button is not pressed + else { + // Button newly released + // Long press event didn't already fire + if (held && !longPressFired) { + // Duration enough for short press + if (!Throttle::isWithinTimespanMs(downSinceMs, durationShortMs)) { + shortPress(); + } + } + + // Record the change of state: button is up + held = false; + longPressFired = false; // Re-Arm: allow another long press + } + + // If thread's job is done, let it sleep + if (!held && !alertingNoMessage) { + Thread::canSleep = true; + return OSThread::disable(); + } + + // Run this method again is a few ms + return durationShortMs; +} + +void ScanAndSelectInput::longPress() +{ + // (If canned messages set) + if (cannedMessageModule->hasMessages()) { + // If module frame displayed already, send the current message + if (cannedMessageModule->shouldDraw()) + raiseEvent(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT); + + // Otherwise, initial long press opens the module frame + else + raiseEvent(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN); + } + + // (If canned messages not set) tell the user + else + alertNoMessage(); +} + +void ScanAndSelectInput::shortPress() +{ + // (If canned messages set) scroll to next message + if (cannedMessageModule->hasMessages()) + raiseEvent(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN); + + // (If canned messages not yet set) tell the user + else + alertNoMessage(); +} + +// Begin running runOnce at regular intervals +// Called from pin change interrupt +void ScanAndSelectInput::enableThread() +{ + Thread::canSleep = false; + OSThread::enabled = true; + OSThread::setIntervalFromNow(0); +} + +// Inform user (screen) that no canned messages have been added +// Automatically dismissed after several seconds +void ScanAndSelectInput::alertNoMessage() +{ + alertingNoMessage = true; + alertingSinceMs = millis(); + + // Graphics code: the alert frame to show on screen + screen->startAlert([](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { + display->setTextAlignment(TEXT_ALIGN_CENTER_BOTH); + display->setFont(FONT_SMALL); + int16_t textX = display->getWidth() / 2; + int16_t textY = display->getHeight() / 2; + display->drawString(textX + x, textY + y, "No Canned Messages"); + }); +} + +// Remove the canned message frame from screen +// Used to dismiss the module frame when user button pressed +// Returns true if the frame was previously displayed, and has now been closed +// Return value consumed by Screen class when determining how to handle user button +bool ScanAndSelectInput::dismissCannedMessageFrame() +{ + if (cannedMessageModule->shouldDraw()) { + raiseEvent(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL); + return true; + } + + return false; +} + +// Feed input to the canned messages module +void ScanAndSelectInput::raiseEvent(_meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar key) +{ + InputEvent e; + e.source = name; + e.inputEvent = key; + notifyObservers(&e); +} + +// Pin change interrupt +void ScanAndSelectInput::handleChangeInterrupt() +{ + // Because we need to detect both press and release (rising and falling edge), the interrupt itself can't determine the + // action. Instead, we start up the thread and get it to read the button for us + + // The instance we're referring to here is created in setupModules() + scanAndSelectInput->enableThread(); +} + +ScanAndSelectInput *scanAndSelectInput = nullptr; // Instantiated in setupModules method. Deleted if unused, or init() fails + +#endif \ No newline at end of file diff --git a/src/input/ScanAndSelect.h b/src/input/ScanAndSelect.h new file mode 100644 index 0000000..0b3e271 --- /dev/null +++ b/src/input/ScanAndSelect.h @@ -0,0 +1,50 @@ +/* + A "single button" input method for Canned Messages + + - Short press to cycle through messages + - Long Press to send + + To use: + - set "allow input source" to "scanAndSelect" + - set the single button's GPIO as either pin A, pin B, or pin Press + + Originally designed to make use of "extra" built-in button on some boards. + Non-intrusive; suitable for use as a default module config. +*/ + +#pragma once +#include "concurrency/OSThread.h" +#include "main.h" + +// Normally these input methods are protected by guarding in setupModules +// In order to have the user button dismiss the canned message frame, this class lightly interacts with the Screen class +#if HAS_SCREEN + +class ScanAndSelectInput : public Observable, public concurrency::OSThread +{ + public: + ScanAndSelectInput(); // No-op constructor, only initializes OSThread base class + bool init(); // Attempt to setup class; true if success. Instance deleted if setup fails + bool dismissCannedMessageFrame(); // Remove the canned message frame from screen. True if frame was open, and now closed. + void alertNoMessage(); // Inform user (screen) that no canned messages have been added + + protected: + int32_t runOnce() override; // Runs at regular intervals, when enabled + void enableThread(); // Begin running runOnce at regular intervals + static void handleChangeInterrupt(); // Calls enableThread from pin change interrupt + void shortPress(); // Code to run when short press fires + void longPress(); // Code to run when long press fires + void raiseEvent(_meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar key); // Feed input to canned message module + + bool held = false; // Have we handled a change in button state? + bool longPressFired = false; // Long press fires while button still held. This bool ensures the release is no-op + uint32_t downSinceMs = 0; // Debouncing for short press, timing for long press + uint8_t pin = -1; // Read from cannned message config during init + + bool alertingNoMessage = false; // Is the "no canned messages" alert shown on screen? + uint32_t alertingSinceMs = 0; // Used to dismiss the "no canned message" alert several seconds +}; + +extern ScanAndSelectInput *scanAndSelectInput; // Instantiated in setupModules method. Deleted if unused, or init() fails + +#endif \ No newline at end of file diff --git a/src/input/SerialKeyboard.cpp b/src/input/SerialKeyboard.cpp new file mode 100644 index 0000000..8d07304 --- /dev/null +++ b/src/input/SerialKeyboard.cpp @@ -0,0 +1,171 @@ +#include "SerialKeyboard.h" +#include "configuration.h" +#include + +#ifdef INPUTBROKER_SERIAL_TYPE +#define CANNED_MESSAGE_MODULE_ENABLE 1 // in case it's not set in the variant file + +#if INPUTBROKER_SERIAL_TYPE == 1 // It's a Chatter +// 3 SHIFT level (lower case, upper case, numbers), up to 4 repeated presses, button number +unsigned char KeyMap[3][4][10] = {{{'.', 'a', 'd', 'g', 'j', 'm', 'p', 't', 'w', ' '}, + {',', 'b', 'e', 'h', 'k', 'n', 'q', 'u', 'x', ' '}, + {'?', 'c', 'f', 'i', 'l', 'o', 'r', 'v', 'y', ' '}, + {'1', '2', '3', '4', '5', '6', 's', '8', 'z', ' '}}, // low case + {{'!', 'A', 'D', 'G', 'J', 'M', 'P', 'T', 'W', ' '}, + {'+', 'B', 'E', 'H', 'K', 'N', 'Q', 'U', 'X', ' '}, + {'-', 'C', 'F', 'I', 'L', 'O', 'R', 'V', 'Y', ' '}, + {'1', '2', '3', '4', '5', '6', 'S', '8', 'Z', ' '}}, // upper case + {{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}, + {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}, + {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}, + {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}}}; // numbers + +#endif + +SerialKeyboard::SerialKeyboard(const char *name) : concurrency::OSThread(name) +{ + this->_originName = name; +} + +void SerialKeyboard::erase() +{ + InputEvent e; + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_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; + } + + 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 = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.source = this->_originName; + // SELECT OR SEND OR CANCEL EVENT + if (!(shiftRegister2 & (1 << 3))) { + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP; + } else if (!(shiftRegister2 & (1 << 2))) { + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT; + e.kbchar = INPUT_BROKER_MSG_RIGHT; + } else if (!(shiftRegister2 & (1 << 1))) { + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; + } else if (!(shiftRegister2 & (1 << 0))) { + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_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 = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK; + e.kbchar = 0x08; + } else { // shift = 1 => TAB + e.inputEvent = 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 = 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 != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + this->notifyObservers(&e); + } + } + 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 new file mode 100644 index 0000000..1480c4d --- /dev/null +++ b/src/input/SerialKeyboard.h @@ -0,0 +1,25 @@ +#pragma once + +#include "InputBroker.h" +#include "concurrency/OSThread.h" + +class SerialKeyboard : public Observable, public concurrency::OSThread +{ + public: + explicit SerialKeyboard(const char *name); + + 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; +}; \ No newline at end of file diff --git a/src/input/SerialKeyboardImpl.cpp b/src/input/SerialKeyboardImpl.cpp new file mode 100644 index 0000000..249b76f --- /dev/null +++ b/src/input/SerialKeyboardImpl.cpp @@ -0,0 +1,21 @@ +#include "SerialKeyboardImpl.h" +#include "InputBroker.h" +#include "configuration.h" + +#ifdef INPUTBROKER_SERIAL_TYPE + +SerialKeyboardImpl *aSerialKeyboardImpl; + +SerialKeyboardImpl::SerialKeyboardImpl() : SerialKeyboard("serialKB") {} + +void SerialKeyboardImpl::init() +{ + if (!INPUTBROKER_SERIAL_TYPE) { + disable(); + return; + } + + 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 new file mode 100644 index 0000000..7f62aa4 --- /dev/null +++ b/src/input/SerialKeyboardImpl.h @@ -0,0 +1,19 @@ +#pragma once +#include "SerialKeyboard.h" +#include "main.h" + +/** + * @brief The idea behind this class to have static methods for the event handlers. + * Check attachInterrupt() at RotaryEncoderInteruptBase.cpp + * Technically you can have as many rotary encoders hardver attached + * 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(); +}; + +extern SerialKeyboardImpl *aSerialKeyboardImpl; \ No newline at end of file diff --git a/src/input/TouchScreenBase.cpp b/src/input/TouchScreenBase.cpp new file mode 100644 index 0000000..03618b3 --- /dev/null +++ b/src/input/TouchScreenBase.cpp @@ -0,0 +1,137 @@ +#include "TouchScreenBase.h" +#include "main.h" + +#ifndef TIME_LONG_PRESS +#define TIME_LONG_PRESS 400 +#endif + +// move a minimum distance over the screen to detect a "swipe" +#ifndef TOUCH_THRESHOLD_X +#define TOUCH_THRESHOLD_X 30 +#endif + +#ifndef TOUCH_THRESHOLD_Y +#define TOUCH_THRESHOLD_Y 20 +#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); + } +} + +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 (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; + e.touchEvent = static_cast(TOUCH_ACTION_DOUBLE_TAP); + LOG_DEBUG("action DOUBLE TAP(%d/%d)", x, y); + } else { + _tapped = true; + } + } else { + _tapped = false; + } + } + } + } + _touchedOld = touched; + + // fire TAP event when no 2nd tap occured within time + 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); + } + + // 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); + } + + return interval; +} + +void TouchScreenBase::hapticFeedback() +{ +#ifdef T_WATCH_S3 + drv.setWaveform(0, 75); + drv.setWaveform(1, 0); // end waveform + drv.go(); +#endif +} \ No newline at end of file diff --git a/src/input/TouchScreenBase.h b/src/input/TouchScreenBase.h new file mode 100644 index 0000000..0b20025 --- /dev/null +++ b/src/input/TouchScreenBase.h @@ -0,0 +1,56 @@ +#pragma once + +#include "InputBroker.h" +#include "concurrency/OSThread.h" +#include "mesh/NodeDB.h" +#include "time.h" + +typedef struct _TouchEvent { + 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); + + 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_DOUBLE_TAP, + TOUCH_ACTION_LONG_PRESS + }; + + virtual int32_t runOnce() override; + + 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(); + + 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 + + const char *_originName; +}; diff --git a/src/input/TouchScreenImpl1.cpp b/src/input/TouchScreenImpl1.cpp new file mode 100644 index 0000000..2019627 --- /dev/null +++ b/src/input/TouchScreenImpl1.cpp @@ -0,0 +1,93 @@ +#include "TouchScreenImpl1.h" +#include "InputBroker.h" +#include "PowerFSM.h" +#include "configuration.h" +#include "modules/ExternalNotificationModule.h" + +#if ARCH_PORTDUINO +#include "platform/portduino/PortduinoGlue.h" +#endif + +TouchScreenImpl1 *touchScreenImpl1; + +TouchScreenImpl1::TouchScreenImpl1(uint16_t width, uint16_t height, bool (*getTouch)(int16_t *, int16_t *)) + : TouchScreenBase("touchscreen1", width, height), _getTouch(getTouch) +{ +} + +void TouchScreenImpl1::init() +{ +#if ARCH_PORTDUINO + if (settingsMap[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); +} + +/** + * @brief forward touchscreen event + * + * @param event + * + * The touchscreen events are translated to input events and reversed + */ +void TouchScreenImpl1::onEvent(const TouchEvent &event) +{ + InputEvent e; + e.source = event.source; + + e.touchX = event.x; + e.touchY = event.y; + + switch (event.touchEvent) { + case TOUCH_ACTION_LEFT: { + e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT); + break; + } + case TOUCH_ACTION_RIGHT: { + e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT); + break; + } + case TOUCH_ACTION_UP: { + e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP); + break; + } + case TOUCH_ACTION_DOWN: { + e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN); + break; + } + case TOUCH_ACTION_DOUBLE_TAP: { + e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT); + break; + } + case TOUCH_ACTION_LONG_PRESS: { + e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL); + break; + } + case TOUCH_ACTION_TAP: { + if (moduleConfig.external_notification.enabled && (externalNotificationModule->nagCycleCutoff != UINT32_MAX)) { + externalNotificationModule->stopNow(); + } else { + powerFSM.trigger(EVENT_INPUT); + } + break; + } + default: + return; + } + this->notifyObservers(&e); +} \ No newline at end of file diff --git a/src/input/TouchScreenImpl1.h b/src/input/TouchScreenImpl1.h new file mode 100644 index 0000000..0c53384 --- /dev/null +++ b/src/input/TouchScreenImpl1.h @@ -0,0 +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); + + protected: + virtual bool getTouch(int16_t &x, int16_t &y); + virtual void onEvent(const TouchEvent &event); + + bool (*_getTouch)(int16_t *, int16_t *); +}; + +extern TouchScreenImpl1 *touchScreenImpl1; diff --git a/src/input/TrackballInterruptBase.cpp b/src/input/TrackballInterruptBase.cpp new file mode 100644 index 0000000..e35da36 --- /dev/null +++ b/src/input/TrackballInterruptBase.cpp @@ -0,0 +1,95 @@ +#include "TrackballInterruptBase.h" +#include "configuration.h" + +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, + char eventDown, char eventUp, char eventLeft, char eventRight, char eventPressed, + void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), + void (*onIntPress)()) +{ + this->_pinDown = pinDown; + this->_pinUp = pinUp; + this->_pinLeft = pinLeft; + this->_pinRight = pinRight; + this->_eventDown = eventDown; + this->_eventUp = eventUp; + this->_eventLeft = eventLeft; + this->_eventRight = eventRight; + this->_eventPressed = eventPressed; + + pinMode(pinPress, INPUT_PULLUP); + pinMode(this->_pinDown, INPUT_PULLUP); + pinMode(this->_pinUp, INPUT_PULLUP); + pinMode(this->_pinLeft, INPUT_PULLUP); + pinMode(this->_pinRight, INPUT_PULLUP); + + attachInterrupt(pinPress, onIntPress, RISING); + attachInterrupt(this->_pinDown, onIntDown, RISING); + attachInterrupt(this->_pinUp, onIntUp, RISING); + attachInterrupt(this->_pinLeft, onIntLeft, RISING); + attachInterrupt(this->_pinRight, onIntRight, RISING); + + LOG_DEBUG("Trackball GPIO initialized (%d, %d, %d, %d, %d)", this->_pinUp, this->_pinDown, this->_pinLeft, this->_pinRight, + pinPress); + + this->setInterval(100); +} + +int32_t TrackballInterruptBase::runOnce() +{ + InputEvent e; + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + + if (this->action == TB_ACTION_PRESSED) { + // LOG_DEBUG("Trackball event Press"); + e.inputEvent = this->_eventPressed; + } else if (this->action == TB_ACTION_UP) { + // LOG_DEBUG("Trackball event UP"); + e.inputEvent = this->_eventUp; + } else if (this->action == TB_ACTION_DOWN) { + // LOG_DEBUG("Trackball event DOWN"); + e.inputEvent = this->_eventDown; + } else if (this->action == TB_ACTION_LEFT) { + // LOG_DEBUG("Trackball event LEFT"); + e.inputEvent = this->_eventLeft; + } else if (this->action == TB_ACTION_RIGHT) { + // LOG_DEBUG("Trackball event RIGHT"); + e.inputEvent = this->_eventRight; + } + + if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + e.source = this->_originName; + e.kbchar = 0x00; + this->notifyObservers(&e); + } + + this->action = TB_ACTION_NONE; + + return 100; +} + +void TrackballInterruptBase::intPressHandler() +{ + this->action = TB_ACTION_PRESSED; +} + +void TrackballInterruptBase::intDownHandler() +{ + this->action = TB_ACTION_DOWN; +} + +void TrackballInterruptBase::intUpHandler() +{ + this->action = TB_ACTION_UP; +} + +void TrackballInterruptBase::intLeftHandler() +{ + this->action = TB_ACTION_LEFT; +} + +void TrackballInterruptBase::intRightHandler() +{ + this->action = TB_ACTION_RIGHT; +} diff --git a/src/input/TrackballInterruptBase.h b/src/input/TrackballInterruptBase.h new file mode 100644 index 0000000..e7fc99f --- /dev/null +++ b/src/input/TrackballInterruptBase.h @@ -0,0 +1,44 @@ +#pragma once + +#include "InputBroker.h" +#include "mesh/NodeDB.h" + +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, char eventDown, char eventUp, + char eventLeft, char eventRight, char eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), + void (*onIntRight)(), void (*onIntPress)()); + void intPressHandler(); + void intDownHandler(); + void intUpHandler(); + void intLeftHandler(); + void intRightHandler(); + + virtual int32_t runOnce() override; + + protected: + enum TrackballInterruptBaseActionType { + TB_ACTION_NONE, + TB_ACTION_PRESSED, + TB_ACTION_UP, + TB_ACTION_DOWN, + TB_ACTION_LEFT, + TB_ACTION_RIGHT + }; + + volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE; + + private: + uint8_t _pinDown = 0; + uint8_t _pinUp = 0; + uint8_t _pinLeft = 0; + uint8_t _pinRight = 0; + char _eventDown = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + char _eventUp = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + char _eventLeft = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + char _eventRight = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + char _eventPressed = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + const char *_originName; +}; diff --git a/src/input/TrackballInterruptImpl1.cpp b/src/input/TrackballInterruptImpl1.cpp new file mode 100644 index 0000000..0a73b83 --- /dev/null +++ b/src/input/TrackballInterruptImpl1.cpp @@ -0,0 +1,54 @@ +#include "TrackballInterruptImpl1.h" +#include "InputBroker.h" +#include "configuration.h" + +TrackballInterruptImpl1 *trackballInterruptImpl1; + +TrackballInterruptImpl1::TrackballInterruptImpl1() : TrackballInterruptBase("trackball1") {} + +void TrackballInterruptImpl1::init() +{ +#if !HAS_TRACKBALL + // Input device is disabled. + return; +#else + uint8_t pinUp = TB_UP; + uint8_t pinDown = TB_DOWN; + uint8_t pinLeft = TB_LEFT; + uint8_t pinRight = TB_RIGHT; + uint8_t pinPress = TB_PRESS; + + char eventDown = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN); + char eventUp = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP); + char eventLeft = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT); + char eventRight = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT); + char eventPressed = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT); + + TrackballInterruptBase::init(pinDown, pinUp, pinLeft, pinRight, pinPress, eventDown, eventUp, eventLeft, eventRight, + eventPressed, TrackballInterruptImpl1::handleIntDown, TrackballInterruptImpl1::handleIntUp, + TrackballInterruptImpl1::handleIntLeft, TrackballInterruptImpl1::handleIntRight, + TrackballInterruptImpl1::handleIntPressed); + inputBroker->registerSource(this); +#endif +} + +void TrackballInterruptImpl1::handleIntDown() +{ + trackballInterruptImpl1->intDownHandler(); +} +void TrackballInterruptImpl1::handleIntUp() +{ + trackballInterruptImpl1->intUpHandler(); +} +void TrackballInterruptImpl1::handleIntLeft() +{ + trackballInterruptImpl1->intLeftHandler(); +} +void TrackballInterruptImpl1::handleIntRight() +{ + trackballInterruptImpl1->intRightHandler(); +} +void TrackballInterruptImpl1::handleIntPressed() +{ + trackballInterruptImpl1->intPressHandler(); +} diff --git a/src/input/TrackballInterruptImpl1.h b/src/input/TrackballInterruptImpl1.h new file mode 100644 index 0000000..36efac6 --- /dev/null +++ b/src/input/TrackballInterruptImpl1.h @@ -0,0 +1,16 @@ +#pragma once +#include "TrackballInterruptBase.h" + +class TrackballInterruptImpl1 : public TrackballInterruptBase +{ + public: + TrackballInterruptImpl1(); + void init(); + 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 new file mode 100644 index 0000000..979489c --- /dev/null +++ b/src/input/UpDownInterruptBase.cpp @@ -0,0 +1,71 @@ +#include "UpDownInterruptBase.h" +#include "configuration.h" + +UpDownInterruptBase::UpDownInterruptBase(const char *name) : concurrency::OSThread(name) +{ + this->_originName = name; +} + +void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, char eventDown, char eventUp, char eventPressed, + void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)()) +{ + this->_pinDown = pinDown; + this->_pinUp = pinUp; + this->_eventDown = eventDown; + this->_eventUp = eventUp; + this->_eventPressed = eventPressed; + + pinMode(pinPress, INPUT_PULLUP); + pinMode(this->_pinDown, INPUT_PULLUP); + pinMode(this->_pinUp, INPUT_PULLUP); + + attachInterrupt(pinPress, onIntPress, RISING); + attachInterrupt(this->_pinDown, onIntDown, RISING); + attachInterrupt(this->_pinUp, onIntUp, RISING); + + LOG_DEBUG("Up/down/press GPIO initialized (%d, %d, %d)", this->_pinUp, this->_pinDown, pinPress); + + this->setInterval(100); +} + +int32_t UpDownInterruptBase::runOnce() +{ + InputEvent e; + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + + if (this->action == UPDOWN_ACTION_PRESSED) { + LOG_DEBUG("GPIO event Press"); + e.inputEvent = this->_eventPressed; + } else if (this->action == UPDOWN_ACTION_UP) { + LOG_DEBUG("GPIO event Up"); + e.inputEvent = this->_eventUp; + } else if (this->action == UPDOWN_ACTION_DOWN) { + LOG_DEBUG("GPIO event Down"); + e.inputEvent = this->_eventDown; + } + + if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + e.source = this->_originName; + e.kbchar = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + this->notifyObservers(&e); + } + + this->action = UPDOWN_ACTION_NONE; + + return 100; +} + +void UpDownInterruptBase::intPressHandler() +{ + this->action = UPDOWN_ACTION_PRESSED; +} + +void UpDownInterruptBase::intDownHandler() +{ + this->action = UPDOWN_ACTION_DOWN; +} + +void UpDownInterruptBase::intUpHandler() +{ + this->action = UPDOWN_ACTION_UP; +} diff --git a/src/input/UpDownInterruptBase.h b/src/input/UpDownInterruptBase.h new file mode 100644 index 0000000..7060a0d --- /dev/null +++ b/src/input/UpDownInterruptBase.h @@ -0,0 +1,30 @@ +#pragma once + +#include "InputBroker.h" +#include "mesh/NodeDB.h" + +class UpDownInterruptBase : public Observable, public concurrency::OSThread +{ + public: + explicit UpDownInterruptBase(const char *name); + void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, char eventDown, char eventUp, char eventPressed, + void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)()); + void intPressHandler(); + void intDownHandler(); + void intUpHandler(); + + int32_t runOnce() override; + + protected: + enum UpDownInterruptBaseActionType { UPDOWN_ACTION_NONE, UPDOWN_ACTION_PRESSED, UPDOWN_ACTION_UP, UPDOWN_ACTION_DOWN }; + + volatile UpDownInterruptBaseActionType action = UPDOWN_ACTION_NONE; + + private: + uint8_t _pinDown = 0; + uint8_t _pinUp = 0; + char _eventDown = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + char _eventUp = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + char _eventPressed = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + const char *_originName; +}; diff --git a/src/input/UpDownInterruptImpl1.cpp b/src/input/UpDownInterruptImpl1.cpp new file mode 100644 index 0000000..7dd1f76 --- /dev/null +++ b/src/input/UpDownInterruptImpl1.cpp @@ -0,0 +1,41 @@ +#include "UpDownInterruptImpl1.h" +#include "InputBroker.h" + +UpDownInterruptImpl1 *upDownInterruptImpl1; + +UpDownInterruptImpl1::UpDownInterruptImpl1() : UpDownInterruptBase("upDown1") {} + +bool UpDownInterruptImpl1::init() +{ + + 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; + + char eventDown = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN); + char eventUp = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP); + char eventPressed = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT); + + UpDownInterruptBase::init(pinDown, pinUp, pinPress, eventDown, eventUp, eventPressed, UpDownInterruptImpl1::handleIntDown, + UpDownInterruptImpl1::handleIntUp, UpDownInterruptImpl1::handleIntPressed); + inputBroker->registerSource(this); + return true; +} + +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 new file mode 100644 index 0000000..4739cd2 --- /dev/null +++ b/src/input/UpDownInterruptImpl1.h @@ -0,0 +1,14 @@ +#pragma once +#include "UpDownInterruptBase.h" + +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 new file mode 100644 index 0000000..c1f35ba --- /dev/null +++ b/src/input/cardKbI2cImpl.cpp @@ -0,0 +1,65 @@ +#include "cardKbI2cImpl.h" +#include "InputBroker.h" +#include "detect/ScanI2CTwoWire.h" +#include "main.h" + +CardKbI2cImpl *cardKbI2cImpl; + +CardKbI2cImpl::CardKbI2cImpl() : KbI2cBase("cardKB") {} + +void CardKbI2cImpl::init() +{ +#if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_PORTDUINO) && !defined(I2C_NO_RESCAN) + if (cardkb_found.address == 0x00) { + LOG_DEBUG("Rescanning for I2C keyboard"); + uint8_t i2caddr_scan[] = {CARDKB_ADDR, TDECK_KB_ADDR, BBQ10_KB_ADDR, MPR121_KB_ADDR}; + uint8_t i2caddr_asize = 4; + auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); + +#if WIRE_INTERFACES_COUNT == 2 + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize); +#endif + 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; + 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; + } + } + 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 + if (cardkb_found.address == 0x00) { + disable(); + return; + } +#endif + inputBroker->registerSource(this); +} \ No newline at end of file diff --git a/src/input/cardKbI2cImpl.h b/src/input/cardKbI2cImpl.h new file mode 100644 index 0000000..811a055 --- /dev/null +++ b/src/input/cardKbI2cImpl.h @@ -0,0 +1,18 @@ +#pragma once +#include "kbI2cBase.h" + +/** + * @brief The idea behind this class to have static methods for the event handlers. + * Check attachInterrupt() at RotaryEncoderInteruptBase.cpp + * Technically you can have as many rotary encoders hardver attached + * 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(); +}; + +extern CardKbI2cImpl *cardKbI2cImpl; \ No newline at end of file diff --git a/src/input/kbI2cBase.cpp b/src/input/kbI2cBase.cpp new file mode 100644 index 0000000..d0f36c3 --- /dev/null +++ b/src/input/kbI2cBase.cpp @@ -0,0 +1,401 @@ +#include "kbI2cBase.h" +#include "configuration.h" +#include "detect/ScanI2C.h" +#include "detect/ScanI2CTwoWire.h" + +extern ScanI2C::DeviceAddress cardkb_found; +extern uint8_t kb_model; + +KbI2cBase::KbI2cBase(const char *name) : concurrency::OSThread(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; +} + +int32_t KbI2cBase::runOnce() +{ + if (!i2cBus) { + switch (cardkb_found.port) { + case ScanI2C::WIRE1: +#if WIRE_INTERFACES_COUNT == 2 + LOG_DEBUG("Using 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); + } + break; +#endif + case ScanI2C::WIRE: + LOG_DEBUG("Using 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); + } + 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 = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.source = this->_originName; + switch (key.key) { + case 'p': // TAB + case 't': // TAB as well + if (is_sym) { + e.inputEvent = ANYKEY; + e.kbchar = 0x09; // TAB Scancode + is_sym = false; // reset sym state after second keypress + } else { + e.inputEvent = ANYKEY; + e.kbchar = key.key; + } + break; + case 'q': // ESC + if (is_sym) { + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL; + e.kbchar = 0x1b; + is_sym = false; // reset sym state after second keypress + } else { + e.inputEvent = ANYKEY; + e.kbchar = key.key; + } + break; + case 0x08: // Back + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK; + e.kbchar = key.key; + break; + case 'e': // sym e + if (is_sym) { + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP; + e.kbchar = INPUT_BROKER_MSG_UP; + is_sym = false; // reset sym state after second keypress + } else { + e.inputEvent = ANYKEY; + e.kbchar = key.key; + } + break; + case 'x': // sym x + if (is_sym) { + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN; + e.kbchar = INPUT_BROKER_MSG_DOWN; + is_sym = false; // reset sym state after second keypress + } else { + e.inputEvent = ANYKEY; + e.kbchar = key.key; + } + break; + case 's': // sym s + if (is_sym) { + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT; + e.kbchar = 0x00; // tweak for destSelect + is_sym = false; // reset sym state after second keypress + } else { + e.inputEvent = ANYKEY; + e.kbchar = key.key; + } + break; + case 'f': // sym f + if (is_sym) { + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT; + e.kbchar = 0x00; // tweak for destSelect + is_sym = false; // reset sym state after second keypress + } else { + e.inputEvent = ANYKEY; + e.kbchar = key.key; + } + break; + case 0x13: // Code scanner says the SYM key is 0x13 + is_sym = !is_sym; + e.inputEvent = 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 = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; + break; + case 0x00: // nopress + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + break; + default: // all other keys + e.inputEvent = ANYKEY; + e.kbchar = key.key; + is_sym = false; // reset sym state after second keypress + break; + } + + if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + this->notifyObservers(&e); + } + } + } + break; + } + case 0x37: { // MPR121 + MPRkeyboard.trigger(); + InputEvent e; + + while (MPRkeyboard.hasEvent()) { + char nextEvent = MPRkeyboard.dequeueEvent(); + e.inputEvent = ANYKEY; + e.kbchar = 0x00; + e.source = this->_originName; + switch (nextEvent) { + case 0x00: // MPR121_NONE + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.kbchar = 0x00; + break; + case 0x90: // MPR121_REBOOT + e.inputEvent = ANYKEY; + e.kbchar = INPUT_BROKER_MSG_REBOOT; + break; + case 0xb4: // MPR121_LEFT + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT; + e.kbchar = 0x00; + break; + case 0xb5: // MPR121_UP + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP; + e.kbchar = 0x00; + break; + case 0xb6: // MPR121_DOWN + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN; + e.kbchar = 0x00; + break; + case 0xb7: // MPR121_RIGHT + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT; + e.kbchar = 0x00; + break; + case 0x1b: // MPR121_ESC + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL; + e.kbchar = 0x1b; + break; + case 0x08: // MPR121_BSP + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK; + e.kbchar = 0x08; + break; + case 0x0d: // MPR121_SELECT + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; + e.kbchar = 0x0d; + break; + default: + if (nextEvent > 127) { + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.kbchar = 0x00; + break; + } + e.inputEvent = ANYKEY; + e.kbchar = nextEvent; + break; + } + if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + LOG_DEBUG("MP121 Notifying: %i Char: %i", e.inputEvent, e.kbchar); + this->notifyObservers(&e); + } + } + 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 = 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 = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_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 = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL; + } else { + e.inputEvent = ANYKEY; + e.kbchar = c; + } + break; + case 0x74: // letter t. if modifier and t pressed call 'tab' + if (is_sym) { + is_sym = false; + e.inputEvent = ANYKEY; + e.kbchar = 0x09; // TAB Scancode + } else { + e.inputEvent = ANYKEY; + e.kbchar = c; + } + break; + case 0x6d: // letter m. Modifier makes it mute notifications + if (is_sym) { + is_sym = false; + e.inputEvent = ANYKEY; + e.kbchar = INPUT_BROKER_MSG_MUTE_TOGGLE; // mute notifications + } else { + e.inputEvent = ANYKEY; + e.kbchar = c; + } + break; + case 0x6f: // letter o(+). Modifier makes screen increase in brightness + if (is_sym) { + is_sym = false; + e.inputEvent = ANYKEY; + e.kbchar = INPUT_BROKER_MSG_BRIGHTNESS_UP; // Increase Brightness code + } else { + e.inputEvent = ANYKEY; + e.kbchar = c; + } + break; + case 0x69: // letter i(-). Modifier makes screen decrease in brightness + if (is_sym) { + is_sym = false; + e.inputEvent = ANYKEY; + e.kbchar = INPUT_BROKER_MSG_BRIGHTNESS_DOWN; // Decrease Brightness code + } else { + e.inputEvent = ANYKEY; + e.kbchar = c; + } + break; + case 0x20: // Space. Send network ping like double press does + if (is_sym) { + is_sym = false; + e.inputEvent = ANYKEY; + e.kbchar = INPUT_BROKER_MSG_SEND_PING; // (fn + space) + } else { + e.inputEvent = ANYKEY; + e.kbchar = c; + } + break; + case 0x67: // letter g. toggle gps + if (is_sym) { + is_sym = false; + e.inputEvent = ANYKEY; + e.kbchar = INPUT_BROKER_MSG_GPS_TOGGLE; + } else { + e.inputEvent = ANYKEY; + e.kbchar = c; + } + break; + case 0x1b: // ESC + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL; + break; + case 0x08: // Back + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK; + e.kbchar = c; + break; + case 0xb5: // Up + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP; + e.kbchar = INPUT_BROKER_MSG_UP; + break; + case 0xb6: // Down + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN; + e.kbchar = INPUT_BROKER_MSG_DOWN; + break; + case 0xb4: // Left + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT; + e.kbchar = INPUT_BROKER_MSG_LEFT; + break; + case 0xb7: // Right + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT; + e.kbchar = INPUT_BROKER_MSG_RIGHT; + 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 = 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 0x90: // fn+r INPUT_BROKER_MSG_REBOOT + case 0x91: // fn+t + case 0x9b: // fn+s INPUT_BROKER_MSG_SHUTDOWN + case 0xac: // fn+m INPUT_BROKER_MSG_MUTE_TOGGLE + case 0x9e: // fn+g INPUT_BROKER_MSG_GPS_TOGGLE + case 0xaf: // fn+space INPUT_BROKER_MSG_SEND_PING + case 0x8b: // fn+del INPUT_BROKEN_MSG_DISMISS_FRAME + case 0xAA: // fn+b INPUT_BROKER_MSG_BLUETOOTH_TOGGLE + // just pass those unmodified + e.inputEvent = ANYKEY; + e.kbchar = c; + break; + case 0x0d: // Enter + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; + break; + case 0x00: // nopress + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + break; + default: // all other keys + if (c > 127) { // bogus key value + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + break; + } + e.inputEvent = ANYKEY; + e.kbchar = c; + is_sym = false; + break; + } + + if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + this->notifyObservers(&e); + } + } + break; + } + default: + LOG_WARN("Unknown kb_model 0x%02x", kb_model); + } + return 300; +} \ No newline at end of file diff --git a/src/input/kbI2cBase.h b/src/input/kbI2cBase.h new file mode 100644 index 0000000..dc2414f --- /dev/null +++ b/src/input/kbI2cBase.h @@ -0,0 +1,25 @@ +#pragma once + +#include "BBQ10Keyboard.h" +#include "InputBroker.h" +#include "MPR121Keyboard.h" +#include "Wire.h" +#include "concurrency/OSThread.h" + +class KbI2cBase : public Observable, public concurrency::OSThread +{ + public: + explicit KbI2cBase(const char *name); + + protected: + virtual int32_t runOnce() override; + + private: + const char *_originName; + + TwoWire *i2cBus = 0; + + BBQ10Keyboard Q10keyboard; + MPR121Keyboard MPRkeyboard; + bool is_sym = false; +}; \ No newline at end of file diff --git a/src/input/kbMatrixBase.cpp b/src/input/kbMatrixBase.cpp new file mode 100644 index 0000000..51815b5 --- /dev/null +++ b/src/input/kbMatrixBase.cpp @@ -0,0 +1,131 @@ +#include "kbMatrixBase.h" +#include "configuration.h" + +#ifdef INPUTBROKER_MATRIX_TYPE + +const byte keys_cols[] = KEYS_COLS; +const byte keys_rows[] = KEYS_ROWS; + +#if INPUTBROKER_MATRIX_TYPE == 1 + +unsigned char KeyMap[3][sizeof(keys_rows)][sizeof(keys_cols)] = {{{' ', '.', 'm', 'n', 'b', 0xb6}, + {0x0d, 'l', 'k', 'j', 'h', 0xb4}, + {'p', 'o', 'i', 'u', 'y', 0xb5}, + {0x08, 'z', 'x', 'c', 'v', 0xb7}, + {'a', 's', 'd', 'f', 'g', 0x09}, + {'q', 'w', 'e', 'r', 't', 0x1a}}, + {// SHIFT + {':', ';', 'M', 'N', 'B', 0xb6}, + {0x0d, 'L', 'K', 'J', 'H', 0xb4}, + {'P', 'O', 'I', 'U', 'Y', 0xb5}, + {0x08, 'Z', 'X', 'C', 'V', 0xb7}, + {'A', 'S', 'D', 'F', 'G', 0x09}, + {'Q', 'W', 'E', 'R', 'T', 0x1a}}, + {// SHIFT-SHIFT + {'_', ',', '>', '<', '"', '{'}, + {'~', '-', '*', '&', '+', '['}, + {'0', '9', '8', '7', '6', '}'}, + {'=', '(', ')', '?', '/', ']'}, + {'!', '@', '#', '$', '%', '\\'}, + {'1', '2', '3', '4', '5', 0x1a}}}; +#endif + +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); + } + 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 = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.source = this->_originName; + switch (key) { + case 0x1b: // ESC + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL; + break; + case 0x08: // Back + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK; + e.kbchar = key; + break; + case 0xb5: // Up + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP; + break; + case 0xb6: // Down + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN; + break; + case 0xb4: // Left + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT; + e.kbchar = key; + break; + case 0xb7: // Right + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT; + e.kbchar = key; + break; + case 0x0d: // Enter + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; + break; + case 0x00: // nopress + e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + break; + case 0x1a: // Shift + shift++; + if (shift > 2) { + shift = 0; + } + break; + default: // all other keys + e.inputEvent = ANYKEY; + e.kbchar = key; + break; + } + if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_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 new file mode 100644 index 0000000..8259fc0 --- /dev/null +++ b/src/input/kbMatrixBase.h @@ -0,0 +1,20 @@ +#pragma once + +#include "InputBroker.h" +#include "concurrency/OSThread.h" + +class KbMatrixBase : public Observable, public concurrency::OSThread +{ + public: + explicit KbMatrixBase(const char *name); + + protected: + virtual int32_t runOnce() override; + + 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 new file mode 100644 index 0000000..0561b16 --- /dev/null +++ b/src/input/kbMatrixImpl.cpp @@ -0,0 +1,20 @@ +#include "kbMatrixImpl.h" +#include "InputBroker.h" + +#ifdef INPUTBROKER_MATRIX_TYPE + +KbMatrixImpl *kbMatrixImpl; + +KbMatrixImpl::KbMatrixImpl() : KbMatrixBase("matrixKB") {} + +void KbMatrixImpl::init() +{ + if (!INPUTBROKER_MATRIX_TYPE) { + disable(); + return; + } + + 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 new file mode 100644 index 0000000..ead4a2d --- /dev/null +++ b/src/input/kbMatrixImpl.h @@ -0,0 +1,19 @@ +#pragma once +#include "kbMatrixBase.h" +#include "main.h" + +/** + * @brief The idea behind this class to have static methods for the event handlers. + * Check attachInterrupt() at RotaryEncoderInteruptBase.cpp + * Technically you can have as many rotary encoders hardver attached + * 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(); +}; + +extern KbMatrixImpl *kbMatrixImpl; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..0ce6b3b --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,1198 @@ +#include "../userPrefs.h" +#include "configuration.h" +#if !MESHTASTIC_EXCLUDE_GPS +#include "GPS.h" +#endif +#include "MeshRadio.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "PowerFSM.h" +#include "PowerMon.h" +#include "ReliableRouter.h" +#include "airtime.h" +#include "buzz.h" + +#include "FSCommon.h" +#include "Led.h" +#include "RTC.h" +#include "SPILock.h" +#include "Throttle.h" +#include "concurrency/OSThread.h" +#include "concurrency/Periodic.h" +#include "detect/ScanI2C.h" +#include "error.h" +#include "power.h" + +#if !MESHTASTIC_EXCLUDE_I2C +#include "detect/ScanI2CTwoWire.h" +#include +#endif +#include "detect/einkScan.h" +#include "graphics/RAKled.h" +#include "graphics/Screen.h" +#include "main.h" +#include "mesh/generated/meshtastic/config.pb.h" +#include "meshUtils.h" +#include "modules/Modules.h" +#include "shutdown.h" +#include "sleep.h" +#include "target_specific.h" +#include +#include + +#ifdef ARCH_ESP32 +#if !MESHTASTIC_EXCLUDE_WEBSERVER +#include "mesh/http/WebServer.h" +#endif +#if !MESHTASTIC_EXCLUDE_BLUETOOTH +#include "nimble/NimbleBluetooth.h" +NimbleBluetooth *nimbleBluetooth = nullptr; +#endif +#endif + +#ifdef ARCH_NRF52 +#include "NRF52Bluetooth.h" +NRF52Bluetooth *nrf52Bluetooth = nullptr; +#endif + +#if HAS_WIFI +#include "mesh/api/WiFiServerAPI.h" +#include "mesh/wifi/WiFiAPClient.h" +#endif + +#if HAS_ETHERNET +#include "mesh/api/ethServerAPI.h" +#include "mesh/eth/ethClient.h" +#endif + +#if !MESHTASTIC_EXCLUDE_MQTT +#include "mqtt/MQTT.h" +#endif + +#include "LLCC68Interface.h" +#include "LR1110Interface.h" +#include "LR1120Interface.h" +#include "LR1121Interface.h" +#include "RF95Interface.h" +#include "SX1262Interface.h" +#include "SX1268Interface.h" +#include "SX1280Interface.h" +#include "detect/LoRaRadioType.h" + +#ifdef ARCH_STM32WL +#include "STM32WLE5JCInterface.h" +#endif + +#if !HAS_RADIO && defined(ARCH_PORTDUINO) +#include "platform/portduino/SimRadio.h" +#endif + +#ifdef ARCH_PORTDUINO +#include "linux/LinuxHardwareI2C.h" +#include "mesh/raspihttp/PiWebServer.h" +#include "platform/portduino/PortduinoGlue.h" +#include +#include +#include +#endif + +#if HAS_BUTTON || defined(ARCH_PORTDUINO) +#include "ButtonThread.h" +#endif + +#include "AmbientLightingThread.h" +#include "PowerFSMThread.h" + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C +#include "motion/AccelerometerThread.h" +AccelerometerThread *accelerometerThread = nullptr; +#endif + +#ifdef HAS_I2S +#include "AudioThread.h" +AudioThread *audioThread = nullptr; +#endif + +#if defined(TCXO_OPTIONAL) +float tcxoVoltage = SX126X_DIO3_TCXO_VOLTAGE; // if TCXO is optional, put this here so it can be changed further down. +#endif + +using namespace concurrency; + +volatile static const char slipstreamTZString[] = USERPREFS_TZ_STRING; + +// We always create a screen object, but we only init it if we find the hardware +graphics::Screen *screen = nullptr; + +// Global power status +meshtastic::PowerStatus *powerStatus = new meshtastic::PowerStatus(); + +// Global GPS status +meshtastic::GPSStatus *gpsStatus = new meshtastic::GPSStatus(); + +// Global Node status +meshtastic::NodeStatus *nodeStatus = new meshtastic::NodeStatus(); + +// Scan for I2C Devices + +/// The I2C address of our display (if found) +ScanI2C::DeviceAddress screen_found = ScanI2C::ADDRESS_NONE; + +// The I2C address of the cardkb or RAK14004 (if found) +ScanI2C::DeviceAddress cardkb_found = ScanI2C::ADDRESS_NONE; +// 0x02 for RAK14004, 0x00 for cardkb, 0x10 for T-Deck +uint8_t kb_model; + +// The I2C address of the RTC Module (if found) +ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE; +// The I2C address of the Accelerometer (if found) +ScanI2C::DeviceAddress accelerometer_found = ScanI2C::ADDRESS_NONE; +// The I2C address of the RGB LED (if found) +ScanI2C::FoundDevice rgb_found = ScanI2C::FoundDevice(ScanI2C::DeviceType::NONE, ScanI2C::ADDRESS_NONE); + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) +ATECCX08A atecc; +#endif + +#ifdef T_WATCH_S3 +Adafruit_DRV2605 drv; +#endif + +// Global LoRa radio type +LoRaRadioType radioType = NO_RADIO; + +bool isVibrating = false; + +bool eink_found = true; + +uint32_t serialSinceMsec; +bool pauseBluetoothLogging = false; + +bool pmu_found; + +#if !MESHTASTIC_EXCLUDE_I2C +// Array map of sensor types with i2c address and wire as we'll find in the i2c scan +std::pair nodeTelemetrySensorsMap[_meshtastic_TelemetrySensorType_MAX + 1] = {}; +#endif + +Router *router = NULL; // Users of router don't care what sort of subclass implements that API + +const char *getDeviceName() +{ + uint8_t dmac[6]; + + 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; +} + +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; + + 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); +} + +uint32_t timeLastPowered = 0; + +static Periodic *ledPeriodic; +static OSThread *powerFSMthread; +static OSThread *ambientLightingThread; + +RadioInterface *rIf = NULL; + +/** + * Some platforms (nrf52) might provide an alterate version that suppresses calling delay from sleep. + */ +__attribute__((weak, noinline)) bool loopCanSleep() +{ + return true; +} + +// Weak empty variant initialization function. +// May be redefined by variant files. +void lateInitVariant() __attribute__((weak)); +void lateInitVariant() {} + +/** + * Print info as a structured log message (for automated log processing) + */ +void printInfo() +{ + LOG_INFO("S:B:%d,%s", HW_VENDOR, optstr(APP_VERSION)); +} +#ifndef PIO_UNIT_TESTING +void setup() +{ + concurrency::hasBeenSetup = true; +#if ARCH_PORTDUINO + SPISettings spiSettings(settingsMap[spiSpeed], MSBFIRST, SPI_MODE0); +#else + 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; + +#ifdef USE_SEGGER + 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 +#else + auto buflen = 256; // this board has a fair amount of ram +#endif + SEGGER_RTT_ConfigUpBuffer(SEGGER_STDOUT_CH, NULL, NULL, buflen, mode); +#endif + +#ifdef DEBUG_PORT + consoleInit(); // Set serial baud rate and init our mesh console +#endif + +#ifdef UNPHONE + unphone.printStore(); +#endif + +#if ARCH_PORTDUINO + struct timeval tv; + tv.tv_sec = time(NULL); + tv.tv_usec = 0; + perhapsSetRTC(RTCQualityNTP, &tv); +#endif + + powerMonInit(); + serialSinceMsec = millis(); + + LOG_INFO("\n\n//\\ E S H T /\\ S T / C\n"); + + initDeepSleep(); + + // power on peripherals +#if defined(PIN_POWER_EN) + pinMode(PIN_POWER_EN, OUTPUT); + digitalWrite(PIN_POWER_EN, HIGH); + // digitalWrite(PIN_POWER_EN1, INPUT); +#endif + +#if defined(LORA_TCXO_GPIO) + 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 +#endif + +#if defined(BIAS_T_ENABLE) + 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); +#endif + +#ifdef RESET_OLED + pinMode(RESET_OLED, OUTPUT); + digitalWrite(RESET_OLED, 1); +#endif + +#ifdef SENSOR_POWER_CTRL_PIN + pinMode(SENSOR_POWER_CTRL_PIN, OUTPUT); + digitalWrite(SENSOR_POWER_CTRL_PIN, SENSOR_POWER_ON); +#endif + +#ifdef SENSOR_GPS_CONFLICT + 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("Waiting for peripherals to stabilize"); + delay(PERIPHERAL_WARMUP_MS); +#endif + +#ifdef BUTTON_PIN +#ifdef ARCH_ESP32 + +#if ESP_ARDUINO_VERSION_MAJOR >= 3 +#ifdef BUTTON_NEED_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 +#endif +#else + 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); +#endif +#endif +#endif +#endif + + OSThread::setup(); + + ledPeriodic = new Periodic("Blink", ledBlinker); + + fsInit(); + +#if defined(_SEEED_XIAO_NRF52840_SENSE_H_) + + pinMode(CHARGE_LED, INPUT); // sets to detect if charge LED is on or off to see if USB is plugged in + + pinMode(HICHG, OUTPUT); + digitalWrite(HICHG, LOW); // 100 mA charging current if set to LOW and 50mA (actually about 20mA) if set to HIGH + + pinMode(BAT_READ, OUTPUT); + digitalWrite(BAT_READ, LOW); // This is pin P0_14 = 14 and by pullling low to GND it provices path to read on pin 32 (P0,31) + // PIN_VBAT the voltage from divider on XIAO board + +#endif + +#if !MESHTASTIC_EXCLUDE_I2C +#if defined(I2C_SDA1) && defined(ARCH_RP2040) + Wire1.setSDA(I2C_SDA1); + Wire1.setSCL(I2C_SCL1); + Wire1.begin(); +#elif defined(I2C_SDA1) && !defined(ARCH_RP2040) + Wire1.begin(I2C_SDA1, I2C_SCL1); +#elif WIRE_INTERFACES_COUNT == 2 + Wire1.begin(); +#endif + +#if defined(I2C_SDA) && defined(ARCH_RP2040) + Wire.setSDA(I2C_SDA); + Wire.setSCL(I2C_SCL); + Wire.begin(); +#elif defined(I2C_SDA) && !defined(ARCH_RP2040) + Wire.begin(I2C_SDA, I2C_SCL); +#elif defined(ARCH_PORTDUINO) + if (settingsStrings[i2cdev] != "") { + LOG_INFO("Using %s as I2C device.", settingsStrings[i2cdev].c_str()); + Wire.begin(settingsStrings[i2cdev].c_str()); + } else { + LOG_INFO("No I2C device configured, skipping."); + } +#elif HAS_WIRE + Wire.begin(); +#endif +#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); +#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); +#endif + +#ifdef T_DECK + // enable keyboard + pinMode(KB_POWERON, OUTPUT); + digitalWrite(KB_POWERON, HIGH); + // There needs to be a delay after power on, give LILYGO-KEYBOARD some startup time + // otherwise keyboard and touch screen will not work + delay(800); +#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 + +#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()); +#if HAS_WIRE + LOG_INFO("Scanning for i2c devices..."); +#endif + +#if defined(I2C_SDA1) && defined(ARCH_RP2040) + Wire1.setSDA(I2C_SDA1); + Wire1.setSCL(I2C_SCL1); + Wire1.begin(); + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1); +#elif defined(I2C_SDA1) && !defined(ARCH_RP2040) + Wire1.begin(I2C_SDA1, I2C_SCL1); + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1); +#elif defined(NRF52840_XXAA) && (WIRE_INTERFACES_COUNT == 2) + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1); +#endif + +#if defined(I2C_SDA) && defined(ARCH_RP2040) + Wire.setSDA(I2C_SDA); + Wire.setSCL(I2C_SCL); + Wire.begin(); + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); +#elif defined(I2C_SDA) && !defined(ARCH_RP2040) + Wire.begin(I2C_SDA, I2C_SCL); + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); +#elif defined(ARCH_PORTDUINO) + if (settingsStrings[i2cdev] != "") { + LOG_INFO("Scanning 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); +#ifdef SENSOR_GPS_CONFLICT + 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(); + } +#endif + + 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; + } + } + +#define UPDATE_FROM_SCANNER(FIND_FN) + + 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) { + 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; + 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); + +/* + * There are a bunch of sensors that have no further logic than to be found and stuffed into the + * nodeTelemetrySensorsMap singleton. This wraps that logic in a temporary scope to declare the temporary field + * "found". + */ + +// Only one supported RGB LED currently +#ifdef HAS_NCP5623 + rgb_found = i2cScanner->find(ScanI2C::DeviceType::NCP5623); +#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(); + } +#endif + +#if !defined(ARCH_PORTDUINO) && !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); +#endif + +#define STRING(S) #S + +#define SCANNER_TO_SENSORS_MAP(SCANNER_T, PB_T) \ + { \ + auto found = i2cScanner->find(SCANNER_T); \ + if (found.type != ScanI2C::DeviceType::NONE) { \ + nodeTelemetrySensorsMap[PB_T].first = found.address.address; \ + nodeTelemetrySensorsMap[PB_T].second = i2cScanner->fetchI2CBus(found.address); \ + LOG_DEBUG("found i2c sensor %s", STRING(PB_T)); \ + } \ + } + + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::BME_680, meshtastic_TelemetrySensorType_BME680) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::BME_280, meshtastic_TelemetrySensorType_BME280) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::BMP_280, meshtastic_TelemetrySensorType_BMP280) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::BMP_3XX, meshtastic_TelemetrySensorType_BMP3XX) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::BMP_085, meshtastic_TelemetrySensorType_BMP085) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::INA260, meshtastic_TelemetrySensorType_INA260) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::INA219, meshtastic_TelemetrySensorType_INA219) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::INA3221, meshtastic_TelemetrySensorType_INA3221) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::MAX17048, meshtastic_TelemetrySensorType_MAX17048) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::MCP9808, meshtastic_TelemetrySensorType_MCP9808) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::MCP9808, meshtastic_TelemetrySensorType_MCP9808) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::SHT31, meshtastic_TelemetrySensorType_SHT31) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::SHTC3, meshtastic_TelemetrySensorType_SHTC3) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::LPS22HB, meshtastic_TelemetrySensorType_LPS22) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::QMC6310, meshtastic_TelemetrySensorType_QMC6310) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::QMI8658, meshtastic_TelemetrySensorType_QMI8658) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::QMC5883L, meshtastic_TelemetrySensorType_QMC5883L) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::HMC5883L, meshtastic_TelemetrySensorType_QMC5883L) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::PMSA0031, meshtastic_TelemetrySensorType_PMSA003I) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::RCWL9620, meshtastic_TelemetrySensorType_RCWL9620) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::VEML7700, meshtastic_TelemetrySensorType_VEML7700) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::TSL2591, meshtastic_TelemetrySensorType_TSL25911FN) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::OPT3001, meshtastic_TelemetrySensorType_OPT3001) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::MLX90632, meshtastic_TelemetrySensorType_MLX90632) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::SHT4X, meshtastic_TelemetrySensorType_SHT4X) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::AHT10, meshtastic_TelemetrySensorType_AHT10) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::DFROBOT_LARK, meshtastic_TelemetrySensorType_DFROBOT_LARK) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948) + SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102) + + i2cScanner.reset(); +#endif + +#ifdef HAS_SDCARD + setupSDCard(); +#endif + + // LED init + +#ifdef LED_PIN + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LED_STATE_ON); // turn on for now +#endif + + // Hello + printInfo(); +#ifdef BUILD_EPOCH + LOG_INFO("Build timestamp: %ld", BUILD_EPOCH); +#endif + +#ifdef ARCH_ESP32 + esp32Setup(); +#endif + +#ifdef ARCH_NRF52 + nrf52Setup(); +#endif + +#ifdef ARCH_RP2040 + rp2040Setup(); +#endif + + initSPI(); // needed here before reading from littleFS + + // 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 we're taking on the repeater role, use flood router and turn off 3V3_S rail because peripherals are not needed + if (config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) { + router = new FloodingRouter(); +#ifdef PIN_3V3_EN + digitalWrite(PIN_3V3_EN, LOW); +#endif + } else + router = new ReliableRouter(); + +#if HAS_BUTTON || defined(ARCH_PORTDUINO) + // Buttons. Moved here cause we need NodeDB to be initialized + buttonThread = new ButtonThread(); +#endif + + // 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: Skipping start melody"); + else + playStartMelody(); + + // 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; +#endif + +#if defined(USE_SH1107_128_64) + screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // keep dimension of 128x64 +#endif + +#if !MESHTASTIC_EXCLUDE_I2C +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) + 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); +#elif !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) + if (rgb_found.type != ScanI2C::DeviceType::NONE) { + ambientLightingThread = new AmbientLightingThread(rgb_found.type); + } +#endif +#endif + +#ifdef T_WATCH_S3 + 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) +#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); +#else // HW_SPI1_DEVICE + SPI.setSCK(LORA_SCK); + SPI.setTX(LORA_MOSI); + SPI.setRX(LORA_MISO); + SPI.begin(false); +#endif // HW_SPI1_DEVICE +#elif !defined(ARCH_ESP32) // ARCH_RP2040 + SPI.begin(); +#else + // ESP32 + 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 + + // Initialize the screen first so we can show the logo while we start up everything else. + screen = new graphics::Screen(screen_found, screen_model, screen_geometry); + + // setup TZ prior to time actions. +#if !MESHTASTIC_EXCLUDE_TZ + LOG_DEBUG("Using 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 { + setenv("TZ", (const char *)slipstreamTZString, 1); + strcpy(config.device.tzdef, (const char *)slipstreamTZString); + } + } + 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) + +#if !MESHTASTIC_EXCLUDE_GPS + // If we're taking on the repeater role, ignore GPS +#ifdef SENSOR_GPS_CONFLICT + if (sensor_detected == false) { +#endif + if (HAS_GPS) { + if (config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER && + config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { + gps = GPS::createGps(); + if (gps) { + gpsStatus->observe(&gps->newStatus); + } else { + LOG_DEBUG("Running without GPS."); + } + } + } +#ifdef SENSOR_GPS_CONFLICT + } +#endif + +#endif + + nodeStatus->observe(&nodeDB->newStatus); + +#ifdef HAS_I2S + LOG_DEBUG("Starting audio thread"); + audioThread = new AudioThread(); +#endif + service = new MeshService(); + service->init(); + + // Now that the mesh service is created, create any modules + setupModules(); + +#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); +#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 +#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) + screen->setup(); +#elif defined(ARCH_PORTDUINO) + if (screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) { + screen->setup(); + } +#else + if (screen_found.port != ScanI2C::I2CPort::NO_I2C) + screen->setup(); +#endif +#endif + + screen->print("Started...\n"); + +#ifdef PIN_PWR_DELAY_MS + // This may be required to give the peripherals time to power up. + delay(PIN_PWR_DELAY_MS); +#endif + +#ifdef ARCH_PORTDUINO + if (settingsMap[use_sx1262]) { + if (!rIf) { + LOG_DEBUG("Attempting to activate sx1262 radio on SPI port %s", settingsStrings[spidev].c_str()); + LockingArduinoHal *RadioLibHAL = + new LockingArduinoHal(SPI, spiSettings, (settingsMap[ch341Quirk] ? settingsMap[busy] : RADIOLIB_NC)); + rIf = new SX1262Interface((LockingArduinoHal *)RadioLibHAL, settingsMap[cs], settingsMap[irq], settingsMap[reset], + settingsMap[busy]); + if (!rIf->init()) { + LOG_ERROR("Failed to find SX1262 radio"); + delete rIf; + exit(EXIT_FAILURE); + } else { + LOG_INFO("SX1262 Radio init succeeded, using SX1262 radio"); + } + } + } else if (settingsMap[use_rf95]) { + if (!rIf) { + LOG_DEBUG("Attempting to activate rf95 radio on SPI port %s", settingsStrings[spidev].c_str()); + LockingArduinoHal *RadioLibHAL = + new LockingArduinoHal(SPI, spiSettings, (settingsMap[ch341Quirk] ? settingsMap[busy] : RADIOLIB_NC)); + rIf = new RF95Interface((LockingArduinoHal *)RadioLibHAL, settingsMap[cs], settingsMap[irq], settingsMap[reset], + settingsMap[busy]); + if (!rIf->init()) { + LOG_ERROR("Failed to find RF95 radio"); + delete rIf; + rIf = NULL; + exit(EXIT_FAILURE); + } else { + LOG_INFO("RF95 Radio init succeeded, using RF95 radio"); + } + } + } else if (settingsMap[use_sx1280]) { + if (!rIf) { + LOG_DEBUG("Attempting to activate sx1280 radio on SPI port %s", settingsStrings[spidev].c_str()); + LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); + rIf = new SX1280Interface((LockingArduinoHal *)RadioLibHAL, settingsMap[cs], settingsMap[irq], settingsMap[reset], + settingsMap[busy]); + if (!rIf->init()) { + LOG_ERROR("Failed to find SX1280 radio"); + delete rIf; + rIf = NULL; + exit(EXIT_FAILURE); + } else { + LOG_INFO("SX1280 Radio init succeeded, using SX1280 radio"); + } + } + } else if (settingsMap[use_sx1268]) { + if (!rIf) { + LOG_DEBUG("Attempting to activate sx1268 radio on SPI port %s", settingsStrings[spidev].c_str()); + LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); + rIf = new SX1268Interface((LockingArduinoHal *)RadioLibHAL, settingsMap[cs], settingsMap[irq], settingsMap[reset], + settingsMap[busy]); + if (!rIf->init()) { + LOG_ERROR("Failed to find SX1268 radio"); + delete rIf; + rIf = NULL; + exit(EXIT_FAILURE); + } else { + LOG_INFO("SX1268 Radio init succeeded, using SX1268 radio"); + } + } + } + +#elif defined(HW_SPI1_DEVICE) + LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI1, spiSettings); +#else // HW_SPI1_DEVICE + LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); +#endif + + // 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("Failed to find STM32WL radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("STM32WL Radio init succeeded, using STM32WL radio"); + radioType = STM32WLx_RADIO; + } + } +#endif + +#if !HAS_RADIO && defined(ARCH_PORTDUINO) + if (!rIf) { + rIf = new SimRadio; + if (!rIf->init()) { + LOG_WARN("Failed to find simulated radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("Using SIMULATED radio!"); + radioType = SIM_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("Failed to find RF95 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("RF95 Radio init succeeded, using RF95 radio"); + 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)) { + rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + if (!rIf->init()) { + LOG_WARN("Failed to find SX1262 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("SX1262 Radio init succeeded, using SX1262 radio"); + 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 + rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + if (!rIf->init()) { + LOG_WARN("Failed to find SX1262 radio with TCXO, Vref %f V", tcxoVoltage); + delete rIf; + rIf = NULL; + tcxoVoltage = 0; // if it fails, set the TCXO voltage to zero for the next attempt + } else { + LOG_WARN("SX1262 Radio init succeeded, TCXO, Vref %f V", tcxoVoltage); + 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 instea + rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + if (!rIf->init()) { + LOG_WARN("Failed to find SX1262 radio with XTAL, Vref %f V", tcxoVoltage); + delete rIf; + rIf = NULL; + tcxoVoltage = SX126X_DIO3_TCXO_VOLTAGE; // if it fails, set the TCXO voltage back for the next radio search + } else { + LOG_INFO("SX1262 Radio init succeeded, XTAL, Vref %f V", tcxoVoltage); + radioType = SX1262_RADIO; + } + } +#endif + +#if defined(USE_SX1268) + 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("Failed to find SX1268 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("SX1268 Radio init succeeded, using SX1268 radio"); + 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("Failed to find LLCC68 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("LLCC68 Radio init succeeded, using LLCC68 radio"); + 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("Failed to find LR1110 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("LR1110 Radio init succeeded, using LR1110 radio"); + 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("Failed to find LR1120 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("LR1120 Radio init succeeded, using LR1120 radio"); + 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("Failed to find LR1121 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("LR1121 Radio init succeeded, using LR1121 radio"); + 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("Failed to find SX1280 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("SX1280 Radio init succeeded, using SX1280 radio"); + radioType = SX1280_RADIO; + } + } +#endif + + // check if the radio chip matches the selected region + if ((config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && (!rIf->wideLora())) { + LOG_WARN("Radio chip does not support 2.4GHz LoRa. Reverting to unset."); + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; + nodeDB->saveToDisk(SEGMENT_CONFIG); + if (!rIf->reconfigure()) { + LOG_WARN("Reconfigure failed, rebooting"); + screen->startAlert("Rebooting..."); + rebootAtMsec = millis() + 5000; + } + } + + lateInitVariant(); // Do board specific init (see extra_variants/README.md for documentation) + +#if !MESHTASTIC_EXCLUDE_MQTT + 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); +#endif + +#ifndef ARCH_PORTDUINO + + // Initialize Wifi +#if HAS_WIFI + initWifi(); +#endif + +#if HAS_ETHERNET + // Initialize Ethernet + initEthernet(); +#endif +#endif + +#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER + // Start web server thread. + webServerThread = new WebServerThread(); +#endif + +#ifdef ARCH_PORTDUINO +#if __has_include() + if (settingsMap[webserverport] != -1) { + piwebServerThread = new PiWebServerThread(); + } +#endif + initApiServer(TCPPort); +#endif + + // Start airtime logger thread. + airTime = new AirTime(); + + 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); + } + + // 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(); + setCPUFast(false); // 80MHz is fine for our slow peripherals +} +#endif +uint32_t rebootAtMsec; // If not zero we will reboot at this time (used to reboot shortly after the update completes) +uint32_t shutdownAtMsec; // If not zero we will shutdown at this time (used to shutdown from python or mobile client) + +// If a thread does something that might need for it to be rescheduled ASAP it can set this flag +// 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; +#if !(MESHTASTIC_EXCLUDE_PKI) + deviceMetadata.hasPKC = true; +#endif + return deviceMetadata; +} +#ifndef PIO_UNIT_TESTING +void loop() +{ + runASAP = false; + +#ifdef ARCH_ESP32 + esp32Loop(); +#endif +#ifdef ARCH_NRF52 + nrf52Loop(); +#endif + powerCommandsCheck(); + +#ifdef DEBUG_STACK + static uint32_t lastPrint = 0; + if (!Throttle::isWithinTimespanMs(lastPrint, 10 * 1000L)) { + lastPrint = millis(); + meshtastic::printThreadInfo("main"); + } +#endif + + service->loop(); + + long delayMsec = mainController.runOrDelay(); + + // We want to sleep as long as possible here - because it saves power + if (!runASAP && loopCanSleep()) { + mainDelay.delay(delayMsec); + } +} +#endif diff --git a/src/main.h b/src/main.h new file mode 100644 index 0000000..5722f7c --- /dev/null +++ b/src/main.h @@ -0,0 +1,89 @@ +#pragma once + +#include "GPSStatus.h" +#include "NodeStatus.h" +#include "PowerStatus.h" +#include "detect/ScanI2C.h" +#include "graphics/Screen.h" +#include "memGet.h" +#include "mesh/generated/meshtastic/config.pb.h" +#include "mesh/generated/meshtastic/telemetry.pb.h" +#include +#include +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) +#include +#endif +#if defined(ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) +#include "nimble/NimbleBluetooth.h" +extern NimbleBluetooth *nimbleBluetooth; +#endif +#ifdef ARCH_NRF52 +#include "NRF52Bluetooth.h" +extern NRF52Bluetooth *nrf52Bluetooth; +#endif + +#if ARCH_PORTDUINO +extern HardwareSPI *DisplaySPI; +extern HardwareSPI *LoraSPI; + +#endif +extern ScanI2C::DeviceAddress screen_found; +extern ScanI2C::DeviceAddress cardkb_found; +extern uint8_t kb_model; +extern ScanI2C::DeviceAddress rtc_found; +extern ScanI2C::DeviceAddress accelerometer_found; +extern ScanI2C::FoundDevice rgb_found; + +extern bool eink_found; +extern bool pmu_found; +extern bool isCharging; +extern bool isUSBPowered; + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) +extern ATECCX08A atecc; +#endif + +#ifdef T_WATCH_S3 +#include +extern Adafruit_DRV2605 drv; +#endif + +#ifdef HAS_I2S +#include "AudioThread.h" +extern AudioThread *audioThread; +#endif + +// Global Screen singleton. +extern graphics::Screen *screen; + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C +#include "motion/AccelerometerThread.h" +extern AccelerometerThread *accelerometerThread; +#endif + +extern bool isVibrating; + +extern int TCPPort; // set by Portduino + +// Return a human readable string of the form "Meshtastic_ab13" +const char *getDeviceName(); + +extern uint32_t timeLastPowered; + +extern uint32_t rebootAtMsec; +extern uint32_t shutdownAtMsec; + +extern uint32_t serialSinceMsec; + +// If a thread does something that might need for it to be rescheduled ASAP it can set this flag +// This will suppress the current delay and instead try to run ASAP. +extern bool runASAP; + +extern bool pauseBluetoothLogging; + +void nrf52Setup(), esp32Setup(), nrf52Loop(), esp32Loop(), rp2040Setup(), clearBonds(), enterDfuMode(); + +meshtastic_DeviceMetadata getDeviceMetadata(); + +// We default to 4MHz SPI, SPI mode 0 +extern SPISettings spiSettings; \ No newline at end of file diff --git a/src/memGet.cpp b/src/memGet.cpp new file mode 100644 index 0000000..ef1102f --- /dev/null +++ b/src/memGet.cpp @@ -0,0 +1,81 @@ +/** + * @file memGet.cpp + * @brief Implementation of MemGet class that provides functions to get memory information. + * + * This file contains the implementation of MemGet class that provides functions to get + * information about free heap, heap size, free psram and psram size. The functions are + * implemented for ESP32 and NRF52 architectures. If the platform does not have heap + * management function implemented, the functions return UINT32_MAX or 0. + */ +#include "memGet.h" +#include "configuration.h" + +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() +{ +#ifdef ARCH_ESP32 + return ESP.getFreeHeap(); +#elif defined(ARCH_NRF52) + return dbgHeapFree(); +#elif defined(ARCH_RP2040) + return rp2040.getFreeHeap(); +#else + // this platform does not have heap management function implemented + return UINT32_MAX; +#endif +} + +/** + * Returns the size of the heap memory in bytes. + * @return uint32_t The size of the heap memory in bytes. + */ +uint32_t MemGet::getHeapSize() +{ +#ifdef ARCH_ESP32 + return ESP.getHeapSize(); +#elif defined(ARCH_NRF52) + return dbgHeapTotal(); +#elif defined(ARCH_RP2040) + return rp2040.getTotalHeap(); +#else + // this platform does not have heap management function implemented + return UINT32_MAX; +#endif +} + +/** + * Returns the amount of free psram memory in bytes. + * + * @return The amount of free psram memory in bytes. + */ +uint32_t MemGet::getFreePsram() +{ +#ifdef ARCH_ESP32 + return ESP.getFreePsram(); +#elif defined(ARCH_PORTDUINO) + return 4194252; +#else + return 0; +#endif +} + +/** + * @brief Returns the size of the PSRAM memory. + * + * @return uint32_t The size of the PSRAM memory. + */ +uint32_t MemGet::getPsramSize() +{ +#ifdef ARCH_ESP32 + return ESP.getPsramSize(); +#elif defined(ARCH_PORTDUINO) + return 4194252; +#else + return 0; +#endif +} \ No newline at end of file diff --git a/src/memGet.h b/src/memGet.h new file mode 100644 index 0000000..130d2ca --- /dev/null +++ b/src/memGet.h @@ -0,0 +1,18 @@ +#pragma once +#ifndef _MT_MEMGET_H +#define _MT_MEMGET_H + +#include + +class MemGet +{ + public: + uint32_t getFreeHeap(); + uint32_t getHeapSize(); + uint32_t getFreePsram(); + uint32_t getPsramSize(); +}; + +extern MemGet memGet; + +#endif diff --git a/src/mesh/Channels.cpp b/src/mesh/Channels.cpp new file mode 100644 index 0000000..b9fe956 --- /dev/null +++ b/src/mesh/Channels.cpp @@ -0,0 +1,406 @@ +#include "Channels.h" +#include "../userPrefs.h" +#include "CryptoEngine.h" +#include "Default.h" +#include "DisplayFormatters.h" +#include "NodeDB.h" +#include "RadioInterface.h" +#include "configuration.h" + +#include + +#if !MESHTASTIC_EXCLUDE_MQTT +#include "mqtt/MQTT.h" +#endif + +Channels channels; + +const char *Channels::adminChannel = "admin"; +const char *Channels::gpioChannel = "gpio"; +const char *Channels::serialChannel = "serial"; +#if !MESHTASTIC_EXCLUDE_MQTT +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; +} + +/** 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)); + + h ^= xorHash(k.bytes, k.length); + + return h; + } +} + +/** + * Validate a channel, fixing any errors as needed + */ +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) + + 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'; + } + + hashes[chIndex] = generateHash(chIndex); + + return ch; +} + +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; + +#ifdef USERPREFS_LORACONFIG_MODEM_PRESET + loraConfig.modem_preset = USERPREFS_LORACONFIG_MODEM_PRESET; +#endif +#ifdef USERPREFS_LORACONFIG_CHANNEL_NUM + loraConfig.channel_num = USERPREFS_LORACONFIG_CHANNEL_NUM; +#endif +} + +/** + * Write a default channel to the specified channel index + */ +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; + + ch.has_settings = true; + ch.role = chIndex == 0 ? meshtastic_Channel_Role_PRIMARY : meshtastic_Channel_Role_SECONDARY; + + 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); +#endif +#ifdef USERPREFS_CHANNEL_0_NAME + strcpy(channelSettings.name, USERPREFS_CHANNEL_0_NAME); +#endif +#ifdef 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; +#endif +#ifdef USERPREFS_CHANNEL_0_DOWNLINK_ENABLED + channelSettings.downlink_enabled = USERPREFS_CHANNEL_0_DOWNLINK_ENABLED; +#endif + 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); +#endif +#ifdef USERPREFS_CHANNEL_1_NAME + strcpy(channelSettings.name, USERPREFS_CHANNEL_1_NAME); +#endif +#ifdef 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; +#endif +#ifdef USERPREFS_CHANNEL_1_DOWNLINK_ENABLED + channelSettings.downlink_enabled = USERPREFS_CHANNEL_1_DOWNLINK_ENABLED; +#endif + 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); +#endif +#ifdef USERPREFS_CHANNEL_2_NAME + strcpy(channelSettings.name, USERPREFS_CHANNEL_2_NAME); +#endif +#ifdef 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; +#endif +#ifdef USERPREFS_CHANNEL_2_DOWNLINK_ENABLED + channelSettings.downlink_enabled = USERPREFS_CHANNEL_2_DOWNLINK_ENABLED; +#endif + break; + default: + break; + } +} + +CryptoKey Channels::getKey(ChannelIndex chIndex) +{ + meshtastic_Channel &ch = getByIndex(chIndex); + const meshtastic_ChannelSettings &channelSettings = ch.settings; + assert(ch.has_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 + + if (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. using 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("Expanding 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; +} + +/** Given a channel index, change to use the crypto key specified by that index + */ +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); + } +} + +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); + } +#else + 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); + + 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]; + } + } + + 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 + // Don't publish messages on the public MQTT broker if we are in event mode + if (strcmp(moduleConfig.mqtt.address, default_mqtt_address) == 0) { + 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); + } 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); + // 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) + * + * This method is called before decoding inbound packets + * + * @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("Skipping channel %d (hash %x) due to invalid hash/index, want=%x", chIndex, getHash(chIndex), + // channelHash); + return false; + } else { + LOG_DEBUG("Using channel %d (hash 0x%x)", chIndex, channelHash); + setCrypto(chIndex); + return true; + } +} + +/** 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 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 new file mode 100644 index 0000000..b0c9b3d --- /dev/null +++ b/src/mesh/Channels.h @@ -0,0 +1,145 @@ +#pragma once + +#include "CryptoEngine.h" +#include "NodeDB.h" +#include "mesh-pb-constants.h" +#include + +/** A channel number (index into the channel table) + */ +typedef uint8_t ChannelIndex; + +/** A low quality hash of the channel PSK and the channel name. created by generateHash(chIndex) + * Used as a hint to limit which PSKs are considered for packet decoding. + */ +typedef uint8_t ChannelHash; + +/** The container/on device API for working with channels */ +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 precomputed hashes for each of our channels, or -1 for invalid + int16_t hashes[MAX_NUM_CHANNELS] = {}; + + public: + Channels() {} + + /// Well known channel names + static const char *adminChannel, *gpioChannel, *serialChannel, *mqttChannel; + + 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 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); + + /** 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 + + /** The index of the primary channel */ + ChannelIndex getPrimaryIndex() const { return primaryIndex; } + + 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 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 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 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(); + + 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); + + /** 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); + + int16_t getHash(ChannelIndex i) { return hashes[i]; } + + /** + * Validate a channel, fixing any errors as needed + */ + meshtastic_Channel &fixupChannel(ChannelIndex chIndex); + + /** + * Writes the default lora config + */ + void initDefaultLoraConfig(); + + /** + * 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); +}; + +/// 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 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 new file mode 100644 index 0000000..282013e --- /dev/null +++ b/src/mesh/CryptoEngine.cpp @@ -0,0 +1,252 @@ +#include "CryptoEngine.h" +// #include "NodeDB.h" +#include "architecture.h" + +#if !(MESHTASTIC_EXCLUDE_PKI) +#include "aes-ccm.h" +#include "meshUtils.h" +#include +#include +#include +#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN) + +/** + * Create a public/private key pair with Curve25519. + * + * @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) +{ + LOG_DEBUG("Generating Curve25519 key pair..."); + Curve25519::dh1(public_key, private_key); + memcpy(pubKey, public_key, sizeof(public_key)); + memcpy(privKey, private_key, sizeof(private_key)); +} + +/** + * regenerate a public key with Curve25519. + * + * @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; + } + return true; +} +#endif +void CryptoEngine::clearKeys() +{ + memset(public_key, 0, sizeof(public_key)); + memset(private_key, 0, sizeof(private_key)); +} + +/** + * Encrypt a packet's payload using a key generated with Curve25519 and SHA256 + * for a specific node. + * + * @param bytes is updated in place + */ +bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, + uint64_t packetNum, size_t numBytes, 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_INFO("Random nonce value: %d", extraNonceTmp); + if (remotePublic.size == 0) { + LOG_DEBUG("Node %d or their public_key not found", toNode); + return false; + } + if (!crypto->setDHPublicKey(remotePublic.bytes)) { + return false; + } + crypto->hash(shared_key, 32); + initNonce(fromNode, packetNum, extraNonceTmp); + + // Calculate the shared secret with the destination node and encrypt + printBytes("Attempting encrypt using nonce: ", nonce, 13); + printBytes("Attempting encrypt using 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; +} + +/** + * Decrypt a packet's payload using a key generated with Curve25519 and SHA256 + * for a specific node. + * + * @param bytes is updated in place + */ +bool CryptoEngine::decryptCurve25519(uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum, + size_t numBytes, uint8_t *bytes, uint8_t *bytesOut) +{ + uint8_t *auth; // set to last 8 bytes of text? + uint32_t extraNonce; // pointer was not really used + auth = bytes + numBytes - 12; + 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; + } + + // Calculate the shared secret with the sending node and decrypt + if (!crypto->setDHPublicKey(remotePublic.bytes)) { + return false; + } + crypto->hash(shared_key, 32); + + initNonce(fromNode, packetNum, extraNonce); + printBytes("Attempting decrypt using nonce: ", nonce, 13); + printBytes("Attempting decrypt using 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); +} + +/** + * Hash arbitrary data using SHA256. + * + * @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::aesSetKey(const uint8_t *key_bytes, size_t key_len) +{ + if (aes) { + 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); +} + +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("Using AES%d key!", k.length * 8); + key = k; +} + +/** + * Encrypt a packet + * + * @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::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) +{ + if (ctr) { + 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); +} + +/** + * 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)); + + // 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; +#endif \ No newline at end of file diff --git a/src/mesh/CryptoEngine.h b/src/mesh/CryptoEngine.h new file mode 100644 index 0000000..32862d9 --- /dev/null +++ b/src/mesh/CryptoEngine.h @@ -0,0 +1,97 @@ +#pragma once +#include "AES.h" +#include "CTR.h" +#include "concurrency/LockGuard.h" +#include "configuration.h" +#include "mesh-pb-constants.h" +#include + +extern concurrency::Lock *cryptLock; + +struct CryptoKey { + uint8_t bytes[32]; + + /// # of bytes, or -1 to mean "invalid key - do not use" + int8_t length; +}; + +/** + * see docs/software/crypto.md for details. + * + */ + +#define MAX_BLOCKSIZE 256 +#define TEST_CURVE25519_FIELD_OPS // Exposes Curve25519::isWeakPoint() for testing keys + +class CryptoEngine +{ + public: +#if !(MESHTASTIC_EXCLUDE_PKI) + uint8_t public_key[32] = {0}; +#endif + + 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); + +#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, uint8_t *bytes, uint8_t *bytesOut); + virtual bool decryptCurve25519(uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum, + size_t numBytes, 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 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); + + /** + * 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: +#endif + /** 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}; +#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); +}; + +extern CryptoEngine *crypto; \ No newline at end of file diff --git a/src/mesh/Default.cpp b/src/mesh/Default.cpp new file mode 100644 index 0000000..653528b --- /dev/null +++ b/src/mesh/Default.cpp @@ -0,0 +1,62 @@ +#include "Default.h" +#include "../userPrefs.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) +{ + 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; +} +/** + * Calculates the scaled value of the configured or default value in ms based on the number of online nodes. + * + * For example a default of 30 minutes (1800 seconds * 1000) would yield: + * 45 nodes = 2475 * 1000 + * 60 nodes = 4500 * 1000 + * 75 nodes = 6525 * 1000 + * 90 nodes = 8550 * 1000 + * @param configured The configured value. + * @param defaultValue The default value. + * @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); + + 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; + + return configured < minValue ? minValue : configured; +} + +uint8_t Default::getConfiguredOrDefaultHopLimit(uint8_t configured) +{ +#if USERPREFS_EVENT_MODE + return (configured > HOP_RELIABLE) ? HOP_RELIABLE : config.lora.hop_limit; +#else + 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 new file mode 100644 index 0000000..2406daf --- /dev/null +++ b/src/mesh/Default.h @@ -0,0 +1,52 @@ +#pragma once +#include +#include +#define ONE_DAY 24 * 60 * 60 +#define ONE_MINUTE_MS 60 * 1000 +#define THIRTY_SECONDS_MS 30 * 1000 +#define FIVE_SECONDS_MS 5 * 1000 + +#define min_default_telemetry_interval_secs 30 * 60 +#define default_gps_update_interval IF_ROUTER(ONE_DAY, 2 * 60) +#define default_telemetry_broadcast_interval_secs IF_ROUTER(ONE_DAY / 2, 60 * 60) +#define default_broadcast_interval_secs IF_ROUTER(ONE_DAY / 2, 15 * 60) +#define default_wait_bluetooth_secs IF_ROUTER(1, 60) +#define default_sds_secs IF_ROUTER(ONE_DAY, UINT32_MAX) // Default to forever super deep sleep +#define default_ls_secs IF_ROUTER(ONE_DAY, 5 * 60) +#define default_min_wake_secs 10 +#define default_screen_on_secs IF_ROUTER(1, 60 * 10) +#define default_node_info_broadcast_secs 3 * 60 * 60 +#define default_neighbor_info_broadcast_secs 6 * 60 * 60 +#define min_node_info_broadcast_secs 60 * 60 // No regular broadcasts of more than once an hour +#define min_neighbor_info_broadcast_secs 2 * 60 * 60 + +#define default_mqtt_address "mqtt.meshtastic.org" +#define default_mqtt_username "meshdev" +#define default_mqtt_password "large4cats" +#define default_mqtt_root "msh" + +#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); + 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: + static float congestionScalingCoefficient(int numOnlineNodes) + { + if (numOnlineNodes <= 40) { + return 1.0; // No scaling for 40 or fewer nodes + } else { + // Sscaling based on number of nodes over 40 + int nodesOverForty = (numOnlineNodes - 40); + return 1.0 + (nodesOverForty * 0.075); // Each number of online node scales by 0.075 + } + } +}; \ No newline at end of file diff --git a/src/mesh/FloodingRouter.cpp b/src/mesh/FloodingRouter.cpp new file mode 100644 index 0000000..6760d70 --- /dev/null +++ b/src/mesh/FloodingRouter.cpp @@ -0,0 +1,79 @@ +#include "FloodingRouter.h" +#include "../userPrefs.h" +#include "configuration.h" +#include "mesh-pb-constants.h" + +FloodingRouter::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 + */ +ErrorCode FloodingRouter::send(meshtastic_MeshPacket *p) +{ + // Add any messages _we_ send to the seen message list (so we will ignore all retransmissions we see) + wasSeenRecently(p); // FIXME, move this to a sniffSent method + + return Router::send(p); +} + +bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) +{ + if (wasSeenRecently(p)) { // Note: this will also add a recent packet record + printPacket("Ignoring dupe incoming msg", p); + rxDupe++; + if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER && + config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER) { + // cancel rebroadcast of this message *if* there was already one, unless we're a router/repeater! + if (Router::cancelSending(p->from, p->id)) + txRelayCanceled++; + } + return true; + } + + return Router::shouldFilterReceived(p); +} + +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); + 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 + } + if (!isToUs(p) && (p->hop_limit > 0) && !isFromUs(p)) { + if (p->id != 0) { + if (isRebroadcaster()) { + meshtastic_MeshPacket *tosend = packetPool.allocCopy(*p); // keep a copy because we will be sending it + + tosend->hop_limit--; // bump down the hop count +#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; + } +#endif + + LOG_INFO("Rebroadcasting received floodmsg"); + // Note: we are careful to resend using the original senders node id + // We are careful not to call our hooked version of send() - because we don't want to check this again + Router::send(tosend); + } else { + LOG_DEBUG("Not rebroadcasting: Role = CLIENT_MUTE or Rebroadcast Mode = NONE"); + } + } else { + LOG_DEBUG("Ignoring 0 id broadcast"); + } + } + // handle the packet as normal + Router::sniffReceived(p, c); +} \ No newline at end of file diff --git a/src/mesh/FloodingRouter.h b/src/mesh/FloodingRouter.h new file mode 100644 index 0000000..0ed2b55 --- /dev/null +++ b/src/mesh/FloodingRouter.h @@ -0,0 +1,61 @@ +#pragma once + +#include "PacketHistory.h" +#include "Router.h" + +/** + * This is a mixin that extends Router with the ability to do Naive Flooding (in the standard mesh protocol sense) + * + * Rules for broadcasting (listing here for now, will move elsewhere eventually): + + If to==BROADCAST and id==0, this is a simple broadcast (0 hops). It will be + sent only by the current node and other nodes will not attempt to rebroadcast + it. + + If to==BROADCAST and id!=0, this is a "naive flooding" broadcast. The initial + node will send it on all local interfaces. + + When other nodes receive this message, they will + first check if their recentBroadcasts table contains the (from, id) pair that + indicates this message. If so, we've already seen it - so we discard it. If + not, we add it to the table and then resend this message on all interfaces. + When resending we are careful to use the "from" ID of the original sender. Not + our own ID. When resending we pick a random delay between 0 and 10 seconds to + decrease the chance of collisions with transmitters we can not even hear. + + 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, protected PacketHistory +{ + private: + bool isRebroadcaster(); + + 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; + + 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; +}; \ No newline at end of file diff --git a/src/mesh/InterfacesTemplates.cpp b/src/mesh/InterfacesTemplates.cpp new file mode 100644 index 0000000..2720e85 --- /dev/null +++ b/src/mesh/InterfacesTemplates.cpp @@ -0,0 +1,38 @@ +#include "LR11x0Interface.cpp" +#include "LR11x0Interface.h" +#include "SX126xInterface.cpp" +#include "SX126xInterface.h" +#include "SX128xInterface.cpp" +#include "SX128xInterface.h" +#include "api/ServerAPI.cpp" +#include "api/ServerAPI.h" + +// We need this declaration for proper linking in derived classes +#if RADIOLIB_EXCLUDE_SX126X != 1 +template class SX126xInterface; +template class SX126xInterface; +template class SX126xInterface; +#endif +#if RADIOLIB_EXCLUDE_SX128X != 1 +template class SX128xInterface; +#endif +#if RADIOLIB_EXCLUDE_LR11X0 != 1 +template class LR11x0Interface; +template class LR11x0Interface; +template class LR11x0Interface; +#endif +#ifdef ARCH_STM32WL +template class SX126xInterface; +#endif + +#if HAS_ETHERNET +#include "api/ethServerAPI.h" +template class ServerAPI; +template class APIServerPort; +#endif + +#if HAS_WIFI +#include "api/WiFiServerAPI.h" +template class ServerAPI; +template class APIServerPort; +#endif \ No newline at end of file diff --git a/src/mesh/LLCC68Interface.cpp b/src/mesh/LLCC68Interface.cpp new file mode 100644 index 0000000..d92ea54 --- /dev/null +++ b/src/mesh/LLCC68Interface.cpp @@ -0,0 +1,11 @@ +#if RADIOLIB_EXCLUDE_SX126X != 1 +#include "LLCC68Interface.h" +#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) +{ +} +#endif \ No newline at end of file diff --git a/src/mesh/LLCC68Interface.h b/src/mesh/LLCC68Interface.h new file mode 100644 index 0000000..1cd23e9 --- /dev/null +++ b/src/mesh/LLCC68Interface.h @@ -0,0 +1,19 @@ +#pragma once +#if RADIOLIB_EXCLUDE_SX126X != 1 +#include "SX126xInterface.h" + +/** + * 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. + * ⚠️⚠️⚠️ + */ +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 new file mode 100644 index 0000000..5dbd3ff --- /dev/null +++ b/src/mesh/LR1110Interface.cpp @@ -0,0 +1,12 @@ +#if RADIOLIB_EXCLUDE_LR11X0 != 1 + +#include "LR1110Interface.h" +#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) +{ +} +#endif \ No newline at end of file diff --git a/src/mesh/LR1110Interface.h b/src/mesh/LR1110Interface.h new file mode 100644 index 0000000..2a2e6e8 --- /dev/null +++ b/src/mesh/LR1110Interface.h @@ -0,0 +1,14 @@ +#pragma once +#if RADIOLIB_EXCLUDE_LR11X0 != 1 +#include "LR11x0Interface.h" + +/** + * 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); +}; +#endif \ No newline at end of file diff --git a/src/mesh/LR1120Interface.cpp b/src/mesh/LR1120Interface.cpp new file mode 100644 index 0000000..a17ac87 --- /dev/null +++ b/src/mesh/LR1120Interface.cpp @@ -0,0 +1,17 @@ +#if RADIOLIB_EXCLUDE_LR11X0 != 1 + +#include "LR1120Interface.h" +#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) +{ +} + +bool LR1120Interface::wideLora() +{ + return true; +} +#endif \ No newline at end of file diff --git a/src/mesh/LR1120Interface.h b/src/mesh/LR1120Interface.h new file mode 100644 index 0000000..d81a480 --- /dev/null +++ b/src/mesh/LR1120Interface.h @@ -0,0 +1,15 @@ +#pragma once +#if RADIOLIB_EXCLUDE_LR11X0 != 1 + +#include "LR11x0Interface.h" +/** + * 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; +}; +#endif \ No newline at end of file diff --git a/src/mesh/LR1121Interface.cpp b/src/mesh/LR1121Interface.cpp new file mode 100644 index 0000000..29bd07d --- /dev/null +++ b/src/mesh/LR1121Interface.cpp @@ -0,0 +1,16 @@ +#if RADIOLIB_EXCLUDE_LR11X0 != 1 +#include "LR1121Interface.h" +#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) +{ +} + +bool LR1121Interface::wideLora() +{ + return true; +} +#endif \ No newline at end of file diff --git a/src/mesh/LR1121Interface.h b/src/mesh/LR1121Interface.h new file mode 100644 index 0000000..ebc5b59 --- /dev/null +++ b/src/mesh/LR1121Interface.h @@ -0,0 +1,16 @@ +#pragma once +#if RADIOLIB_EXCLUDE_LR11X0 != 1 + +#include "LR11x0Interface.h" + +/** + * 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; +}; +#endif \ No newline at end of file diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp new file mode 100644 index 0000000..d0c1a1f --- /dev/null +++ b/src/mesh/LR11x0Interface.cpp @@ -0,0 +1,302 @@ +#if RADIOLIB_EXCLUDE_LR11X0 != 1 +#include "LR11x0Interface.h" +#include "Throttle.h" +#include "configuration.h" +#include "error.h" +#include "mesh/NodeDB.h" +#ifdef LR11X0_DIO_AS_RF_SWITCH +#include "rfswitch.h" +#else +static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; +static const Module::RfSwitchMode_t rfswitch_table[] = { + {LR11x0::MODE_STBY, {}}, {LR11x0::MODE_RX, {}}, {LR11x0::MODE_TX, {}}, {LR11x0::MODE_TX_HP, {}}, + {LR11x0::MODE_TX_HF, {}}, {LR11x0::MODE_GNSS, {}}, {LR11x0::MODE_WIFI, {}}, END_OF_MODE_TABLE, +}; +#endif + +#ifdef ARCH_PORTDUINO +#include "PortduinoGlue.h" +#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) +#ifndef LR1110_MAX_POWER +#define LR1110_MAX_POWER 22 +#endif + +// the 2.4G part maxes at 13dBm + +#ifndef LR1120_MAX_POWER +#define LR1120_MAX_POWER 13 +#endif + +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); +} + +/// 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() +{ +#ifdef LR11X0_POWER_EN + pinMode(LR11X0_POWER_EN, OUTPUT); + digitalWrite(LR11X0_POWER_EN, HIGH); +#endif + +// FIXME: correct logic to default to not using TCXO if no voltage is specified for LR11x0_DIO3_TCXO_VOLTAGE +#if !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"); +#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) +#endif + + RadioLibInterface::init(); + + if (power > LR1110_MAX_POWER) // Clamp power to maximum defined level + power = 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 + } + + limitPower(); + +#ifdef LR11X0_RF_SWITCH_SUBGHZ + pinMode(LR11X0_RF_SWITCH_SUBGHZ, OUTPUT); + digitalWrite(LR11X0_RF_SWITCH_SUBGHZ, getFreq() < 1e9 ? HIGH : LOW); + LOG_DEBUG("Setting 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("Setting 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; + + 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); + + 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(); + +#ifdef LR11X0_DIO_AS_RF_SWITCH + bool dioAsRfSwitch = true; +#elif defined(ARCH_PORTDUINO) + bool dioAsRfSwitch = false; + if (settingsMap[dio2_as_rf_switch]) { + dioAsRfSwitch = true; + } +#else + bool dioAsRfSwitch = false; +#endif + + if (dioAsRfSwitch) { + lora.setRfSwitchTable(rfswitch_dio_pins, rfswitch_table); + LOG_DEBUG("Setting DIO RF switch", 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 + + return res == RADIOLIB_ERR_NONE; +} + +template bool LR11x0Interface::reconfigure() +{ + RadioLibInterface::reconfigure(); + + // 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); + + 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); + + // Hmm - seems to lower SNR when the signal levels are high. Leaving off for now... + // TODO: Confirm gain registers are okay now + // err = lora.setRxGain(true); + // 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.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; + + err = lora.setOutputPower(power); + assert(err == RADIOLIB_ERR_NONE); + + startReceive(); // restart receiving + + return RADIOLIB_ERR_NONE; +} + +template void INTERRUPT_ATTR LR11x0Interface::disableInterrupt() +{ + lora.clearIrqAction(); +} + +template void LR11x0Interface::setStandby() +{ + checkNotification(); // handle any pending interrupts before we force standby + + int err = lora.standby(); + + if (err != RADIOLIB_ERR_NONE) { + LOG_DEBUG("LR11x0 standby failed with error %d", 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(); +} + +/** + * 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()); +} + +/** We override to turn on transmitter power as needed. + */ +template void LR11x0Interface::configHardwareForSend() +{ + RadioLibInterface::configHardwareForSend(); +} + +// For power draw measurements, helpful to force radio to stay sleeping +// #define SLEEP_ONLY + +template void LR11x0Interface::startReceive() +{ +#ifdef SLEEP_ONLY + sleep(); +#else + + setStandby(); + + 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. + // Furthermore, we need the PREAMBLE_DETECTED and HEADER_VALID IRQ flag to detect whether we are actively receiving + int err = lora.startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS, RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); + assert(err == RADIOLIB_ERR_NONE); + + RadioLibInterface::startReceive(); + + // 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 + int16_t result; + + setStandby(); + result = lora.scanChannel(); + if (result == RADIOLIB_LORA_DETECTED) + return true; + + assert(result != RADIOLIB_ERR_WRONG_MODEM); + + 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::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); + + // 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); +#endif + + return true; +} +#endif \ No newline at end of file diff --git a/src/mesh/LR11x0Interface.h b/src/mesh/LR11x0Interface.h new file mode 100644 index 0000000..4829ddc --- /dev/null +++ b/src/mesh/LR11x0Interface.h @@ -0,0 +1,69 @@ +#pragma once +#if RADIOLIB_EXCLUDE_LR11X0 != 1 +#include "RadioLibInterface.h" + +/** + * \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); + + /// 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; + + /// 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; } + + protected: + /** + * Specific module instance + */ + T lora; + + /** + * 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); } + + /** 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; + + /** + * Start waiting to receive a message + */ + virtual void startReceive() 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; + + virtual void setStandby() override; +}; +#endif \ No newline at end of file diff --git a/src/mesh/MemoryPool.h b/src/mesh/MemoryPool.h new file mode 100644 index 0000000..d30404b --- /dev/null +++ b/src/mesh/MemoryPool.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include + +#include "PointerQueue.h" + +template class Allocator +{ + + public: + virtual ~Allocator() {} + + /// Return a queable object which has been prefilled with zeros. Panic if no buffer is available + /// Note: this method is safe to call from regular OR ISR code + T *allocZeroed() + { + T *p = allocZeroed(0); + + assert(p); // FIXME panic instead + 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); + assert(p); + + if (p) + *p = src; + return p; + } + + /// Return a buffer for use by others + virtual void release(T *p) = 0; + + protected: + // Alloc some storage + virtual T *alloc(TickType_t maxWait) = 0; +}; + +/** + * 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 + { + assert(p); + free(p); + } + + protected: + // Alloc some storage + virtual T *alloc(TickType_t maxWait) override + { + T *p = (T *)malloc(sizeof(T)); + assert(p); + return p; + } +}; diff --git a/src/mesh/MeshModule.cpp b/src/mesh/MeshModule.cpp new file mode 100644 index 0000000..a8de540 --- /dev/null +++ b/src/mesh/MeshModule.cpp @@ -0,0 +1,298 @@ +#include "MeshModule.h" +#include "Channels.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "configuration.h" +#include "modules/RoutingModule.h" +#include + +std::vector *MeshModule::modules; + +const meshtastic_MeshPacket *MeshModule::currentRequest; + +/** + * 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 + */ +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(); + + modules->push_back(this); +} + +void MeshModule::setup() {} + +MeshModule::~MeshModule() +{ + assert(0); // FIXME - remove from list of modules once someone needs this feature +} + +meshtastic_MeshPacket *MeshModule::allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, + uint8_t hopStart, uint8_t hopLimit) +{ + meshtastic_Routing c = meshtastic_Routing_init_default; + + 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); + + p->priority = meshtastic_MeshPacket_Priority_ACK; + + p->hop_limit = routingModule->getHopLimitForResponse(hopStart, 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; +} + +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); + + return r; +} + +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; + + currentReply = NULL; // No reply 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); + + for (auto i = modules->begin(); i != modules->end(); ++i) { + auto &pi = **i; + + 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); + + 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("Discarding 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 (isDecoded && mp.decoded.want_response && toUs) { + if (currentReply) { + printPacket("Sending 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, mp.hop_start, + mp.hop_limit); + } + } + + 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; +} + +/** 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"); + } +} + +/** 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.hop_start, to.hop_limit); + + // 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() +{ + + std::vector modulesWithUIFrames; + 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; +} + +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; + } + } + } + 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; +} +#endif diff --git a/src/mesh/MeshModule.h b/src/mesh/MeshModule.h new file mode 100644 index 0000000..7929ba9 --- /dev/null +++ b/src/mesh/MeshModule.h @@ -0,0 +1,219 @@ +#pragma once + +#include "mesh/Channels.h" +#include "mesh/MeshTypes.h" +#include + +#if HAS_SCREEN +#include +#include +#endif + +/** handleReceived return enumeration + * + * Use ProcessMessage::CONTINUE to allows other modules to process a message. + * + * Use ProcessMessage::STOP to stop further message processing. + */ +enum class ProcessMessage { + CONTINUE = 0, + STOP = 1, +}; + +/** + * Used by modules to return the result of the AdminMessage handling. + * If request is handled, then module should return HANDLED, + * If response is also prepared for the request, then HANDLED_WITH_RESPONSE + * should be returned. + */ +enum class AdminMessageHandleResult { + 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, attempting to remain on the same frame throughout + } action = REDRAW_ONLY; + + // 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. + * + * 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; + + public: + /** Constructor + * name is for debugging output + */ + MeshModule(const char *_name); + + virtual ~MeshModule(); + + /** For use only by MeshService + */ + static void callModules(meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO); + + static std::vector GetMeshModulesWithUIFrames(); + 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? +#endif + 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; + + /** 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; + + /* 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 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 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(); + + /** + * @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 + + @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) {} + + /** 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; } + + meshtastic_MeshPacket *allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, + uint8_t hopStart = 0, uint8_t hopLimit = 0); + + /// 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; + }; + +#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; } +#else + 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; + + 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); +}; + +/** 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); \ No newline at end of file diff --git a/src/mesh/MeshPacketQueue.cpp b/src/mesh/MeshPacketQueue.cpp new file mode 100644 index 0000000..99ef41c --- /dev/null +++ b/src/mesh/MeshPacketQueue.cpp @@ -0,0 +1,130 @@ +#include "MeshPacketQueue.h" +#include "NodeDB.h" +#include "configuration.h" +#include + +#include + +/// @return the priority of the specified packet +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); + 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(); +} + +/** + * 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; + } + } + } +} + +/** enqueue a packet, return false if full */ +bool MeshPacketQueue::enqueue(meshtastic_MeshPacket *p) +{ + // no space - try to replace a lower priority packet in the queue + if (queue.size() >= maxLen) { + return replaceLowerPriorityPacket(p); + } + + // 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; + } + + auto *p = queue.front(); + queue.erase(queue.begin()); // Remove the highest-priority packet + return p; +} + +meshtastic_MeshPacket *MeshPacketQueue::getFront() +{ + if (empty()) { + return NULL; + } + + auto *p = queue.front(); + 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) +{ + for (auto it = queue.begin(); it != queue.end(); it++) { + auto p = (*it); + if (getFrom(p) == from && p->id == id) { + queue.erase(it); + return p; + } + } + + return NULL; +} + +/** Attempt to find and remove a packet from this queue. Returns the packet which was removed from the queue */ +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->priority < p->priority) { + // Remove the back packet + packetPool.release(backPacket); + queue.pop_back(); + // 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 new file mode 100644 index 0000000..3c28fc5 --- /dev/null +++ b/src/mesh/MeshPacketQueue.h @@ -0,0 +1,40 @@ +#pragma once + +#include "MeshTypes.h" + +#include + +/** + * A priority queue of packets + */ +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); + + public: + explicit MeshPacketQueue(size_t _maxLen); + + /** enqueue a packet, return false if full */ + bool enqueue(meshtastic_MeshPacket *p); + + /** return true if the queue is empty */ + bool empty(); + + /** 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; } + + meshtastic_MeshPacket *dequeue(); + + meshtastic_MeshPacket *getFront(); + + /** 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); +}; diff --git a/src/mesh/MeshRadio.h b/src/mesh/MeshRadio.h new file mode 100644 index 0000000..f2514ee --- /dev/null +++ b/src/mesh/MeshRadio.h @@ -0,0 +1,25 @@ +#pragma once + +#include "MemoryPool.h" +#include "MeshTypes.h" +#include "PointerQueue.h" +#include "configuration.h" + +// 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 +}; + +extern const RegionInfo regions[]; +extern const RegionInfo *myRegion; + +extern void initRegion(); \ No newline at end of file diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp new file mode 100644 index 0000000..d224c05 --- /dev/null +++ b/src/mesh/MeshService.cpp @@ -0,0 +1,421 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_GPS +#include "GPS.h" +#endif + +#include "../concurrency/Periodic.h" +#include "BluetoothCommon.h" // needed for updateBatteryLevel, FIXME, eventually when we pull mesh out into a lib we shouldn't be whacking bluetooth from here +#include "MeshService.h" +#include "NodeDB.h" +#include "PowerFSM.h" +#include "RTC.h" +#include "TypeConversions.h" +#include "main.h" +#include "mesh-pb-constants.h" +#include "modules/NodeInfoModule.h" +#include "modules/PositionModule.h" +#include "power.h" +#include +#include + +#if ARCH_PORTDUINO +#include "PortduinoGlue.h" +#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) + +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) +*/ + +MeshService *service; + +static MemoryDynamic staticMqttClientProxyMessagePool; + +static MemoryDynamic staticQueueStatusPool; + +static MemoryDynamic staticClientNotificationPool; + +Allocator &mqttClientProxyMessagePool = staticMqttClientProxyMessagePool; + +Allocator &clientNotificationPool = staticClientNotificationPool; + +Allocator &queueStatusPool = staticQueueStatusPool; + +#include "Router.h" + +MeshService::MeshService() + : toPhoneQueue(MAX_RX_TOPHONE), toPhoneQueueStatusQueue(MAX_RX_TOPHONE), toPhoneMqttProxyQueue(MAX_RX_TOPHONE), + toPhoneClientNotificationQueue(MAX_RX_TOPHONE / 2) +{ + lastQueueStatus = {0, 0, 16, 0}; +} + +void MeshService::init() +{ +#if HAS_GPS + 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 + + nodeDB->updateFrom(*mp); // update our DB state based off sniffing every RX packet from the radio + 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."); // because this potentially a Repeater which will + // ignore our request for its NodeInfo + } else if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && !nodeDB->getMeshNode(mp->from)->has_user && + nodeInfoModule) { + LOG_INFO("Heard new node on channel %d, sending NodeInfo and asking for a response.", mp->channel); + if (airTime->isTxAllowedChannelUtil(true)) { + nodeInfoModule->sendOurNodeInfo(mp->from, true, mp->channel); + } else { + LOG_DEBUG("Skip sending NodeInfo due to > 25 percent channel util."); + } + } + + printPacket("Forwarding to phone", mp); + sendToPhone(packetPool.allocCopy(*mp)); + + 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; + } +} + +/// The radioConfig object just changed, call this to force the hw to change to the new settings +bool 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 + bool didReset = 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); + + return didReset; +} + +/// 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(); + } +} + +// 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); + } + 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 + */ +void MeshService::handleToRadio(meshtastic_MeshPacket &p) +{ +#if defined(ARCH_PORTDUINO) && !HAS_RADIO + // Simulates device is receiving a packet via the LoRa chip + if (p.decoded.portnum == meshtastic_PortNum_SIMULATOR_APP) { + // 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 protobuf for simulator message!"); + } + // Let SimRadio receive as if it did via its LoRa chip + SimRadio::instance->startReceive(&p); + return; + } +#endif + p.from = 0; // We don't let phones assign nodenums to their sent messages + + 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 + // (so we update our nodedb for the local node) + + // Send the packet into the mesh + + sendToMesh(packetPool.allocCopy(p), 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, discarding 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 && ccToPhone) { // Check if p is not released in case it couldn't be sent + sendToPhone(packetPool.allocCopy(*p)); + } +} + +bool MeshService::trySendPosition(NodeNum dest, bool wantReplies) +{ + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); + + assert(node); + + if (hasValidPosition(node)) { +#if HAS_GPS && !MESHTASTIC_EXCLUDE_GPS + if (positionModule) { + LOG_INFO("Sending 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("Sending 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); + +#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, discarding oldest"); + meshtastic_MeshPacket *d = toPhoneQueue.dequeuePtr(0); + if (d) + releaseToPool(d); + } else { + LOG_WARN("ToPhone queue is full, dropping packet."); + releaseToPool(p); + fromNum++; // Make sure to notify observers in case they are reconnected so they can get the packets + return; + } + } + + assert(toPhoneQueue.enqueue(p, 0)); + fromNum++; +} + +void MeshService::sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m) +{ + LOG_DEBUG("Sending mqtt message on topic '%s' to client for proxy", m->topic); + if (toPhoneMqttProxyQueue.numFree() == 0) { + LOG_WARN("MqttClientProxyMessagePool queue is full, discarding oldest"); + meshtastic_MqttClientProxyMessage *d = toPhoneMqttProxyQueue.dequeuePtr(0); + if (d) + releaseMqttClientProxyMessageToPool(d); + } + + assert(toPhoneMqttProxyQueue.enqueue(m, 0)); + fromNum++; +} + +void MeshService::sendClientNotification(meshtastic_ClientNotification *n) +{ + LOG_DEBUG("Sending client notification to phone"); + if (toPhoneClientNotificationQueue.numFree() == 0) { + LOG_WARN("ClientNotification queue is full, discarding oldest"); + meshtastic_ClientNotification *d = toPhoneClientNotificationQueue.dequeuePtr(0); + if (d) + releaseClientNotificationToPool(d); + } + + assert(toPhoneClientNotificationQueue.enqueue(n, 0)); + fromNum++; +} + +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; + } + + 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; + + 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_EXTRAVERBOSE + LOG_DEBUG("onGPSchanged() - lost validLocation"); +#endif + } + // Used fixed position if configured regardless of GPS lock + if (config.position.fixed_position) { + LOG_WARN("Using fixed position"); + pos = TypeConversions::ConvertToPosition(node->position); + } + + // 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); + + // Update our current position in the local DB + nodeDB->updatePosition(nodeDB->getNodeNum(), pos, RX_SRC_LOCAL); + + 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; +} diff --git a/src/mesh/MeshService.h b/src/mesh/MeshService.h new file mode 100644 index 0000000..1ccca4e --- /dev/null +++ b/src/mesh/MeshService.h @@ -0,0 +1,168 @@ +#pragma once + +#include +#include +#include + +#include "GPSStatus.h" +#include "MemoryPool.h" +#include "MeshRadio.h" +#include "MeshTypes.h" +#include "Observer.h" +#include "PointerQueue.h" +#if defined(ARCH_PORTDUINO) && !HAS_RADIO +#include "../platform/portduino/SimRadio.h" +#endif +#if defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) +#if !MESHTASTIC_EXCLUDE_STOREFORWARD +#include "modules/StoreForwardModule.h" +#endif +#endif + +extern Allocator &queueStatusPool; +extern Allocator &mqttClientProxyMessagePool; +extern Allocator &clientNotificationPool; + +/** + * Top level app for this service. keeps the mesh, the radio config and the queue of received packets. + * + */ +class MeshService +{ +#if HAS_GPS + 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 + PointerQueue toPhoneQueue; + + // keep list of QueueStatus packets to be send to the phone + PointerQueue toPhoneQueueStatusQueue; + + // keep list of MqttClientProxyMessages to be send to the client for delivery + PointerQueue toPhoneMqttProxyQueue; + + // keep list of ClientNotifications to be send to the client (phone) + PointerQueue toPhoneClientNotificationQueue; + + // 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; + + /// Updated in loop() to detect when fromNum changes + uint32_t oldFromNum = 0; + + public: + 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; + } + /// 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; + + MeshService(); + + void init(); + + /// 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); } + + /// 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 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); } + + // 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 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); } + + /** + * 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 + */ + bool 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); + + /// 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); + + /** 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(); + + /// Send a packet to the phone + void sendToPhone(meshtastic_MeshPacket *p); + + /// Send an MQTT message to the phone for client proxying + void sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m); + + /// Send a ClientNotification to the phone + void sendClientNotification(meshtastic_ClientNotification *cn); + + bool isToPhoneQueueEmpty(); + + ErrorCode sendQueueStatusToPhone(const meshtastic_QueueStatus &qs, ErrorCode res, uint32_t mesh_packet_id); + + uint32_t GetTimeSinceMeshPacket(const meshtastic_MeshPacket *mp); + + 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); +#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; +}; + +extern MeshService *service; diff --git a/src/mesh/MeshTypes.h b/src/mesh/MeshTypes.h new file mode 100644 index 0000000..7a3d785 --- /dev/null +++ b/src/mesh/MeshTypes.h @@ -0,0 +1,62 @@ +#pragma once + +// low level types + +#include "MemoryPool.h" +#include "mesh/mesh-pb-constants.h" +#include + +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 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 +#define ERRNO_DISABLED 34 // the interface is disabled +#define ID_COUNTER_MASK (UINT32_MAX >> 22) // mask to select the counter portion of the ID + +/* + * 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 +}; + +/** + * 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. + **/ +#define HOP_MAX 7 + +/// We normally just use max 3 hops for sending reliable messages +#define HOP_RELIABLE 3 + +typedef int ErrorCode; + +/// Alloc and free packets to our global, ISR safe pool +extern Allocator &packetPool; + +/** + * 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); + +// Returns true if the packet originated from the local node +bool isFromUs(const meshtastic_MeshPacket *p); + +// Returns true if the packet is destined to us +bool isToUs(const meshtastic_MeshPacket *p); + +/* Some clients might not properly set priority, therefore we fix it here. */ +void fixPriority(meshtastic_MeshPacket *p); + +bool isBroadcast(uint32_t dest); \ No newline at end of file diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp new file mode 100644 index 0000000..69cd631 --- /dev/null +++ b/src/mesh/NodeDB.cpp @@ -0,0 +1,1277 @@ +#include "../userPrefs.h" +#include "configuration.h" +#if !MESHTASTIC_EXCLUDE_GPS +#include "GPS.h" +#endif +#include "../detect/ScanI2C.h" +#include "Channels.h" +#include "CryptoEngine.h" +#include "Default.h" +#include "FSCommon.h" +#include "MeshRadio.h" +#include "NodeDB.h" +#include "PacketHistory.h" +#include "PowerFSM.h" +#include "RTC.h" +#include "Router.h" +#include "SafeFile.h" +#include "TypeConversions.h" +#include "error.h" +#include "main.h" +#include "mesh-pb-constants.h" +#include "meshUtils.h" +#include "modules/NeighborInfoModule.h" +#include +#include +#include +#include +#include +#include + +#ifdef ARCH_ESP32 +#if HAS_WIFI +#include "mesh/wifi/WiFiAPClient.h" +#endif +#include "SPILock.h" +#include "modules/StoreForwardModule.h" +#include +#include +#include +#include +#include +#include +#endif + +#ifdef ARCH_PORTDUINO +#include "modules/StoreForwardModule.h" +#include "platform/portduino/PortduinoGlue.h" +#endif + +#ifdef ARCH_NRF52 +#include +#include +#endif + +NodeDB *nodeDB = nullptr; + +// we have plenty of ram so statically alloc this tempbuf (for now) +EXT_RAM_BSS_ATTR meshtastic_DeviceState devicestate; +meshtastic_MyNodeInfo &myNodeInfo = devicestate.my_node; +meshtastic_LocalConfig config; +meshtastic_LocalModuleConfig moduleConfig; +meshtastic_ChannelFile channelFile; + +bool meshtastic_DeviceState_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->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 + * might have changed is incremented. Allows others to detect they might now be on a new channel. + */ +uint32_t radioGeneration; + +// FIXME - move this somewhere else +extern void getMacAddr(uint8_t *dmac); + +/** + * + * Normally userids are unique and start with +country code to look like Signal phone numbers. + * But there are some special ids used when we haven't yet been configured by a user. In that case + * we use !macaddr (no colons). + */ +meshtastic_User &owner = devicestate.owner; +meshtastic_Position localPosition = meshtastic_Position_init_default; +meshtastic_CriticalErrorCode error_code = + meshtastic_CriticalErrorCode_NONE; // For the error code, only show values from this boot (discard value from flash) +uint32_t error_address = 0; + +static uint8_t ourMacAddr[6]; + +NodeDB::NodeDB() +{ + LOG_INFO("Initializing NodeDB"); + loadFromDisk(); + cleanupMeshDB(); + + uint32_t devicestateCRC = crc32Buffer(&devicestate, sizeof(devicestate)); + uint32_t configCRC = crc32Buffer(&config, sizeof(config)); + uint32_t channelFileCRC = crc32Buffer(&channelFile, sizeof(channelFile)); + + int saveWhat = 0; + bool hasUniqueId = false; + // Get device unique id +#if defined(ARCH_ESP32) && defined(ESP_EFUSE_OPTIONAL_UNIQUE_ID) + 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; + hasUniqueId = true; + } 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; + hasUniqueId = true; +#else + // FIXME - implement for other platforms +#endif + // Uncomment below to print the device id + // if (hasUniqueId) { + // 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(); + + // 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)); + + // Include our owner in the node db under our nodenum + meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum()); + if (!config.has_security) { + config.has_security = true; + config.security.serial_enabled = config.device.serial_enabled; + config.security.is_managed = config.device.is_managed; + } + +#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) + 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("Generating 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); + } +#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); + } +#endif + + info->user = TypeConversions::ConvertToUserLite(owner); + info->has_user = true; + +#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); +#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); + + // 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("Coercing 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); + } + + if (devicestateCRC != crc32Buffer(&devicestate, sizeof(devicestate))) + saveWhat |= SEGMENT_DEVICESTATE; + 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; + } + 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 + */ +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(); +} + +// 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; +} + +bool NodeDB::resetRadioConfig(bool factory_reset) +{ + bool didFactoryReset = false; + + radioGeneration++; + + if (factory_reset) { + didFactoryReset = factoryReset(); + } + + if (channelFile.channels_count != MAX_NUM_CHANNELS) { + LOG_INFO("Setting default channel and radio preferences!"); + + channels.initDefaults(); + } + + channels.onConfigChanged(); + + // Update the global myRegion + initRegion(); + + if (didFactoryReset) { + LOG_INFO("Rebooting due to factory reset"); + screen->startAlert("Rebooting..."); + rebootAtMsec = millis() + (5 * 1000); + } + + return didFactoryReset; +} + +bool NodeDB::factoryReset(bool eraseBleBonds) +{ + LOG_INFO("Performing factory reset!"); + // first, remove the "/prefs" (this removes most prefs) + rmDir("/prefs"); +#ifdef FSCom + if (FSCom.exists("/static/rangetest.csv") && !FSCom.remove("/static/rangetest.csv")) { + LOG_ERROR("Could not remove rangetest.csv file"); + } +#endif + // second, install default state (this will deal with the duplicate mac address issue) + 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("Erasing BLE bonds"); +#ifdef ARCH_ESP32 + // This will erase what's in NVS including ssl keys, persistent variables and ble pairing + nvs_flash_erase(); +#endif +#ifdef ARCH_NRF52 + Bluefruit.begin(); + LOG_INFO("Clearing 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; +} + +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("Installing 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; +#ifdef USERPREFS_CONFIG_LORA_REGION + config.lora.region = USERPREFS_CONFIG_LORA_REGION; +#else + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; +#endif +#ifdef USERPREFS_LORACONFIG_MODEM_PRESET + config.lora.modem_preset = USERPREFS_LORACONFIG_MODEM_PRESET; +#else + config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; +#endif + config.lora.hop_limit = HOP_RELIABLE; +#ifdef USERPREFS_CONFIG_LORA_IGNORE_MQTT + config.lora.ignore_mqtt = USERPREFS_CONFIG_LORA_IGNORE_MQTT; +#else + config.lora.ignore_mqtt = false; +#endif +#ifdef USERPREFS_USE_ADMIN_KEY + memcpy(config.security.admin_key[0].bytes, USERPREFS_ADMIN_KEY, 32); + config.security.admin_key[0].size = 32; + config.security.admin_key_count = 1; +#endif + 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; +#endif +#ifdef GPS_POWER_TOGGLE + config.device.disable_triple_click = false; +#else + config.device.disable_triple_click = true; +#endif +#if defined(USERPREFS_CONFIG_GPS_MODE) + config.position.gps_mode = USERPREFS_CONFIG_GPS_MODE; +#elif !HAS_GPS || defined(T_DECK) || defined(TLORA_T3S3_EPAPER) + 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; +#else + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; +#endif + config.position.position_broadcast_smart_enabled = true; + 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(); + strncpy(config.network.ntp_server, "meshtastic.pool.ntp.org", 32); + // FIXME: Default to bluetooth capability of platform as default + config.bluetooth.enabled = true; + 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) + bool hasScreen = true; +#elif ARCH_PORTDUINO + bool hasScreen = false; + if (settingsMap[displayPanel]) + hasScreen = true; + else + hasScreen = screen_found.port != ScanI2C::I2CPort::NO_I2C; +#else + bool hasScreen = screen_found.port != ScanI2C::I2CPort::NO_I2C; +#endif + config.bluetooth.mode = hasScreen ? meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN + : meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN; + // 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); + +#ifdef DISPLAY_FLIP_SCREEN + config.display.flip_screen = true; +#endif +#ifdef RAK4630 + config.display.wake_on_tap_or_motion = true; +#endif +#ifdef T_WATCH_S3 + config.display.screen_on_secs = 30; + config.display.wake_on_tap_or_motion = true; +#endif +#ifdef HELTEC_VISION_MASTER_E290 + // Orient so that LoRa antenna faces up + config.display.flip_screen = true; +#endif + + initConfigIntervals(); +} + +void NodeDB::initConfigIntervals() +{ + config.position.gps_update_interval = default_gps_update_interval; + config.position.position_broadcast_secs = default_broadcast_interval_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; + +#if defined(T_WATCH_S3) || defined(T_DECK) + config.power.is_power_saving = true; + config.display.screen_on_secs = 30; + config.power.wait_bluetooth_secs = 30; +#endif +} + +void NodeDB::installDefaultModuleConfig() +{ + LOG_INFO("Installing 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; +#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 = 60; +#endif +#if defined(RAK4630) || defined(RAK11310) + // Default to RAK led pin 2 (blue) + moduleConfig.external_notification.enabled = true; + moduleConfig.external_notification.output = PIN_LED2; + 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 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; + moduleConfig.external_notification.nag_timeout = 60; +#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; +#endif +#ifdef BUTTON_SECONDARY_CANNEDMESSAGES + // Use a board's second built-in button as input source for canned messages + moduleConfig.canned_message.enabled = true; + moduleConfig.canned_message.inputbroker_pin_press = BUTTON_PIN_SECONDARY; + strcpy(moduleConfig.canned_message.allow_input_source, "scanAndSelect"); +#endif + + moduleConfig.has_canned_message = true; + + strncpy(moduleConfig.mqtt.address, default_mqtt_address, sizeof(moduleConfig.mqtt.address)); + strncpy(moduleConfig.mqtt.username, default_mqtt_username, sizeof(moduleConfig.mqtt.username)); + strncpy(moduleConfig.mqtt.password, default_mqtt_password, sizeof(moduleConfig.mqtt.password)); + strncpy(moduleConfig.mqtt.root, default_mqtt_root, sizeof(moduleConfig.mqtt.root)); + moduleConfig.mqtt.encryption_enabled = true; + + 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_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(); + } else if (role == meshtastic_Config_DeviceConfig_Role_REPEATER) { + config.display.screen_on_secs = 1; + } else if (role == meshtastic_Config_DeviceConfig_Role_SENSOR) { + 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_TAK_TRACKER) { + 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 = UINT32_MAX; + config.position.position_broadcast_smart_enabled = false; + config.position.position_broadcast_secs = UINT32_MAX; + moduleConfig.neighbor_info.update_interval = UINT32_MAX; + moduleConfig.telemetry.device_update_interval = UINT32_MAX; + moduleConfig.telemetry.environment_update_interval = UINT32_MAX; + moduleConfig.telemetry.air_quality_interval = UINT32_MAX; + moduleConfig.telemetry.health_update_interval = UINT32_MAX; + } +} + +void NodeDB::initModuleConfigIntervals() +{ + // Zero out telemetry intervals so that they coalesce to defaults in Default.h + moduleConfig.telemetry.device_update_interval = 0; + 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("Installing default ChannelFile"); + memset(&channelFile, 0, sizeof(meshtastic_ChannelFile)); + channelFile.version = DEVICESTATE_CUR_VER; +} + +void NodeDB::resetNodes() +{ + if (!config.position.fixed_position) + clearLocalPosition(); + numMeshNodes = 1; + std::fill(devicestate.node_db_lite.begin() + 1, devicestate.node_db_lite.end(), meshtastic_NodeInfoLite()); + devicestate.has_rx_text_message = false; + devicestate.has_rx_waypoint = false; + 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(devicestate.node_db_lite.begin() + numMeshNodes, devicestate.node_db_lite.begin() + numMeshNodes + 1, + meshtastic_NodeInfoLite()); + LOG_DEBUG("NodeDB::removeNodeByNum purged %d entries. Saving changes...", removed); + saveDeviceStateToDisk(); +} + +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); +} + +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; + } + } + meshNodes->at(newPos++) = meshNodes->at(i); + } else { + removed++; + } + } + numMeshNodes -= removed; + std::fill(devicestate.node_db_lite.begin() + numMeshNodes, devicestate.node_db_lite.begin() + numMeshNodes + removed, + meshtastic_NodeInfoLite()); + LOG_DEBUG("cleanupMeshDB purged %d entries", removed); +} + +void NodeDB::installDefaultDeviceState() +{ + LOG_INFO("Installing default DeviceState"); + // memset(&devicestate, 0, sizeof(meshtastic_DeviceState)); + + numMeshNodes = 0; + meshNodes = &devicestate.node_db_lite; + + // init our devicestate with valid flags so protobuf writing/reading will work + devicestate.has_my_node = true; + devicestate.has_owner = true; + // devicestate.node_db_lite_count = 0; + 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), USERPREFS_CONFIG_OWNER_LONG_NAME); +#else + snprintf(owner.long_name, sizeof(owner.long_name), "Meshtastic %02x%02x", ourMacAddr[4], ourMacAddr[5]); +#endif +#ifdef USERPREFS_CONFIG_OWNER_SHORT_NAME + snprintf(owner.short_name, sizeof(owner.short_name), USERPREFS_CONFIG_OWNER_SHORT_NAME); +#else + snprintf(owner.short_name, sizeof(owner.short_name), "%02x%02x", ourMacAddr[4], ourMacAddr[5]); +#endif + snprintf(owner.id, sizeof(owner.id), "!%08x", getNodeNum()); // Default node ID now based on nodenum + memcpy(owner.macaddr, ourMacAddr, sizeof(owner.macaddr)); +} + +// We reserve a few nodenums for future use +#define NUM_RESERVED 4 + +/** + * 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]; + } + + 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("Using nodenum 0x%x ", nodeNum); + + myNodeInfo.my_node_num = nodeNum; +} + +static const char *prefFileName = "/prefs/db.proto"; +static const char *configFileName = "/prefs/config.proto"; +static const char *moduleConfigFileName = "/prefs/module.proto"; +static const char *channelFileName = "/prefs/channels.proto"; + +/** 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; +#ifdef FSCom + + auto f = FSCom.open(filename, FILE_O_READ); + + if (f) { + LOG_INFO("Loading %s", filename); + pb_istream_t stream = {&readcb, &f, protoSize}; + + 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_ERROR("Could not open / read %s", filename); + } +#else + LOG_ERROR("ERROR: Filesystem not implemented"); + state = LoadFileResult::NO_FILESYSTEM; +#endif + return state; +} + +void NodeDB::loadFromDisk() +{ + devicestate.version = + 0; // 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. + + // static DeviceState scratch; We no longer read into a tempbuf because this structure is 15KB of valuable RAM + auto state = loadProto(prefFileName, sizeof(meshtastic_DeviceState) + MAX_NUM_NODES * sizeof(meshtastic_NodeInfo), + 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 (devicestate.version < DEVICESTATE_MIN_VER) { + LOG_WARN("Devicestate %d is old, discarding", devicestate.version); + installDefaultDeviceState(); + } else { + LOG_INFO("Loaded saved devicestate version %d, with nodecount: %d", devicestate.version, devicestate.node_db_lite.size()); + meshNodes = &devicestate.node_db_lite; + numMeshNodes = devicestate.node_db_lite.size(); + } + meshNodes->resize(MAX_NUM_NODES); + + 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, discarding", config.version); + installDefaultConfig(true); + } else { + LOG_INFO("Loaded saved config version %d", config.version); + } + } + + 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, discarding", moduleConfig.version); + installDefaultModuleConfig(); + } else { + 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 + } else { + if (channelFile.version < DEVICESTATE_MIN_VER) { + LOG_WARN("channelFile %d is old, discarding", 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; + + saveToDisk(SEGMENT_MODULECONFIG); + } +} + +/** 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) +{ +#ifdef ARCH_ESP32 + concurrency::LockGuard g(spiLock); +#endif + bool okay = false; +#ifdef FSCom + auto f = SafeFile(filename, fullAtomic); + + LOG_INFO("Saving %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; + } + + bool writeSucceeded = f.close(); + + if (!okay || !writeSucceeded) { + LOG_ERROR("Can't write prefs!"); + } +#else + LOG_ERROR("ERROR: Filesystem not implemented"); +#endif + return okay; +} + +bool NodeDB::saveChannelsToDisk() +{ +#ifdef FSCom + FSCom.mkdir("/prefs"); +#endif + return saveProto(channelFileName, meshtastic_ChannelFile_size, &meshtastic_ChannelFile_msg, &channelFile); +} + +bool NodeDB::saveDeviceStateToDisk() +{ +#ifdef FSCom + FSCom.mkdir("/prefs"); +#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(prefFileName, sizeof(devicestate) + numMeshNodes * meshtastic_NodeInfoLite_size, &meshtastic_DeviceState_msg, + &devicestate, false); +} + +bool NodeDB::saveToDiskNoRetry(int saveWhat) +{ + bool success = true; + +#ifdef FSCom + FSCom.mkdir("/prefs"); +#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(); + } + + return success; +} + +bool NodeDB::saveToDisk(int 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 + FSCom.format(); + +#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(); + + 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; +} + +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; + + return delta; +} + +#define NUM_ONLINE_SECS (60 * 60 * 2) // 2 hrs to consider someone offline + +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++; + } + + return numseen; +} + +#include "MeshModule.h" +#include "Throttle.h" + +/** 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; + } + + 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); + + // 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); + + // 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; + } + + 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 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; + } + + LOG_DEBUG("old user %s/%s, channel=%d", info->user.long_name, info->user.short_name, info->channel); +#if !(MESHTASTIC_EXCLUDE_PKI) + if (p.public_key.size > 0) { + printBytes("Incoming Pubkey: ", p.public_key.bytes, 32); + if (info->user.public_key.size > 0) { // if we have a key for this user already, don't overwrite with a new one + LOG_INFO("Public Key set for node, not updating!"); + // we copy the key into the incoming packet, to prevent overwrite + memcpy(p.public_key.bytes, info->user.public_key.bytes, 32); + } else { + LOG_INFO("Updating Node Pubkey!"); + } + } +#endif + + // 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("updating changed=%d user %s/%s, channel=%d", changed, info->user.long_name, info->user.short_name, info->channel); + info->has_user = true; + + if (changed) { + updateGUIforNode = info; + powerFSM.trigger(EVENT_NODEDB_UPDATED); + notifyObservers(true); // Force an update whether or not our node counts have changed + + // We just changed something about the user, store our DB + Throttle::execute( + &lastNodeDbSave, ONE_MINUTE_MS, []() { nodeDB->saveToDisk(SEGMENT_DEVICESTATE); }, + []() { LOG_DEBUG("Deferring NodeDB saveToDisk for now"); }); // since we saved less than a minute ago + } + + 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.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 + if (mp.hop_start != 0 && mp.hop_limit <= mp.hop_start) { + info->has_hops_away = true; + info->hops_away = mp.hop_start - mp.hop_limit; + } + } +} + +uint8_t NodeDB::getMeshNodeChannel(NodeNum n) +{ + const meshtastic_NodeInfoLite *info = getMeshNode(n); + if (!info) { + return 0; // defaults to PRIMARY + } + return info->channel; +} + +/// 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); + + return NULL; +} + +/// Find a node in our DB, create an empty NodeInfo if missing +meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n) +{ + meshtastic_NodeInfoLite *lite = getMeshNode(n); + + if (!lite) { + if ((numMeshNodes >= MAX_NUM_NODES) || (memGet.getFreeHeap() < MINIMUM_SAFE_FREE_HEAP)) { + if (screen) + screen->print("Warn: node database full!\nErasing oldest entry\n"); + LOG_WARN("Node database full with %i nodes and %i 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 node + if (!meshNodes->at(i).is_favorite && 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).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; + } + // 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)++); + + // everything is missing except the nodenum + memset(lite, 0, sizeof(*lite)); + lite->num = n; + LOG_INFO("Adding node to database with %i nodes and %i bytes free!", numMeshNodes, memGet.getFreeHeap()); + } + + return lite; +} + +/// Record an error that should be reported via analytics +void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, const char *filename) +{ + // Print error to screen and serial port + String lcd = String("Critical error ") + code + "!\n"; + if (screen) + screen->print(lcd.c_str()); + if (filename) { + LOG_ERROR("NOTE! Recording critical error %d at %s:%lu", code, filename, address); + } else { + LOG_ERROR("NOTE! Recording critical error %d, address=0x%lx", code, 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 +#ifdef ARCH_PORTDUINO + LOG_ERROR("A critical failure occurred, portduino is exiting..."); + exit(2); +#endif +} diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h new file mode 100644 index 0000000..eb33a2f --- /dev/null +++ b/src/mesh/NodeDB.h @@ -0,0 +1,241 @@ +#pragma once + +#include "Observer.h" +#include +#include +#include +#include + +#include "MeshTypes.h" +#include "NodeStatus.h" +#include "configuration.h" +#include "mesh-pb-constants.h" +#include "mesh/generated/meshtastic/mesh.pb.h" // For CriticalErrorCode + +/* +DeviceState versions used to be defined in the .proto file but really only this function cares. So changed to a +#define here. +*/ + +#define SEGMENT_CONFIG 1 +#define SEGMENT_MODULECONFIG 2 +#define SEGMENT_DEVICESTATE 4 +#define SEGMENT_CHANNELS 8 + +#define DEVICESTATE_CUR_VER 23 +#define DEVICESTATE_MIN_VER 22 + +extern meshtastic_DeviceState devicestate; +extern meshtastic_ChannelFile channelFile; +extern meshtastic_MyNodeInfo &myNodeInfo; +extern meshtastic_LocalConfig config; +extern meshtastic_LocalModuleConfig moduleConfig; +extern meshtastic_User &owner; +extern meshtastic_Position localPosition; + +/// 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); + +/// Given a packet, return how many seconds in the past (vs now) it was received +uint32_t sinceReceived(const meshtastic_MeshPacket *p); + +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 +}; + +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 + + 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; + + /// 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); + + /** 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 + * + * @return true if the config was completely reset, in that case, we should send it back to the client + */ + bool resetRadioConfig(bool factory_reset = 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); + + /** 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 user info and channel for this node based on received user data + */ + bool updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex = 0); + + /// @return our node number + NodeNum getNodeNum() { return myNodeInfo.my_node_num; } + + /// 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?) + */ + + /// pick a provisional nodenum we hope no one is using + void pickNewNodeNum(); + + // 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); + + void initConfigIntervals(), initModuleConfigIntervals(), resetNodes(), removeNodeByNum(NodeNum nodeNum); + + 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); + + void installRoleDefaults(meshtastic_Config_DeviceConfig_Role role); + + const meshtastic_NodeInfoLite *readNextMeshNode(uint32_t &readIndex); + + meshtastic_NodeInfoLite *getMeshNodeByIndex(size_t x) + { + assert(x < numMeshNodes); + return &meshNodes->at(x); + } + + meshtastic_NodeInfoLite *getMeshNode(NodeNum n); + size_t getNumMeshNodes() { return numMeshNodes; } + + void clearLocalPosition(); + + void setLocalPosition(meshtastic_Position position, bool timeOnly = false) + { + if (timeOnly) { + LOG_DEBUG("Setting 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("Setting local position: lat=%i lon=%i time=%u timestamp=%u", position.latitude_i, position.longitude_i, + position.time, position.timestamp); + localPosition = position; + } + + private: + uint32_t lastNodeDbSave = 0; // when we last saved our db to flash + /// Find a node in our DB, create an empty NodeInfoLite if missing + meshtastic_NodeInfoLite *getOrCreateMeshNode(NodeNum n); + + /// 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); + } + + /// read our db from flash + void loadFromDisk(); + + /// purge db entries without user info + void cleanupMeshDB(); + + /// Reinit device state from scratch (not loading from disk) + void installDefaultDeviceState(), installDefaultChannels(), installDefaultConfig(bool preserveKey), + installDefaultModuleConfig(); + + /// write to flash + /// @return true if the save was successful + bool saveToDiskNoRetry(int saveWhat); + + bool saveChannelsToDisk(); + bool saveDeviceStateToDisk(); +}; + +extern NodeDB *nodeDB; + +/* + If is_router is set, we use a number of different default values + + # FIXME - after tuning, move these params into the on-device defaults based on is_router and is_power_saving + + # prefs.position_broadcast_secs = FIXME possibly broadcast only once an hr + prefs.wait_bluetooth_secs = 1 # Don't stay in bluetooth mode + # try to stay in light sleep one full day, then briefly wake and sleep again + + prefs.ls_secs = oneday + + prefs.position_broadcast_secs = 12 hours # send either position or owner every 12hrs + + # get a new GPS position once per day + prefs.gps_update_interval = oneday + + prefs.is_power_saving = True +*/ + +/// Sometimes we will have Position objects that only have a time, so check for +/// valid lat/lon +static inline bool hasValidPosition(const meshtastic_NodeInfoLite *n) +{ + return n->has_position && (n->position.latitude_i != 0 || n->position.longitude_i != 0); +} + +/** The current change # for radio settings. Starts at 0 on boot and any time the radio settings + * might have changed is incremented. Allows others to detect they might now be on a new channel. + */ +extern uint32_t radioGeneration; + +extern meshtastic_CriticalErrorCode error_code; + +/* + * A numeric error address (nonzero if available) + */ +extern uint32_t error_address; + +#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. \ No newline at end of file diff --git a/src/mesh/PacketHistory.cpp b/src/mesh/PacketHistory.cpp new file mode 100644 index 0000000..8d49bce --- /dev/null +++ b/src/mesh/PacketHistory.cpp @@ -0,0 +1,78 @@ +#include "PacketHistory.h" +#include "configuration.h" +#include "mesh-pb-constants.h" + +#ifdef ARCH_PORTDUINO +#include "platform/portduino/PortduinoGlue.h" +#endif +#include "Throttle.h" + +PacketHistory::PacketHistory() +{ + recentPackets.reserve(MAX_NUM_NODES); // Prealloc the worst case # of records - to prevent heap fragmentation + // setup our periodic task +} + +/** + * Update recentBroadcasts and return true if we have already seen this packet + */ +bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpdate) +{ + if (p->id == 0) { + LOG_DEBUG("Ignoring message with zero id"); + return false; // Not a floodable message ID, so we don't care + } + + PacketRecord r; + r.id = p->id; + r.sender = getFrom(p); + r.rxTimeMsec = millis(); + + auto found = recentPackets.find(r); + bool seenRecently = (found != recentPackets.end()); // found not equal to .end() means packet was seen recently + + if (seenRecently && + !Throttle::isWithinTimespanMs(found->rxTimeMsec, FLOOD_EXPIRE_TIME)) { // Check whether found packet has already expired + recentPackets.erase(found); // Erase and pretend packet has not been seen recently + found = recentPackets.end(); + seenRecently = false; + } + + if (seenRecently) { + LOG_DEBUG("Found existing packet record for fr=0x%x,to=0x%x,id=0x%x", p->from, p->to, p->id); + } + + if (withUpdate) { + if (found != recentPackets.end()) { // delete existing to updated timestamp (re-insert) + recentPackets.erase(found); // as unsorted_set::iterator is const (can't update timestamp - so re-insert..) + } + recentPackets.insert(r); + printPacket("Add packet record", p); + } + + // Capacity is reerved, so only purge expired packets if recentPackets fills past 90% capacity + // Expiry is normally dealt with after having searched/found a packet (above) + if (recentPackets.size() > (MAX_NUM_NODES * 0.9)) { + clearExpiredRecentPackets(); + } + + return seenRecently; +} + +/** + * Iterate through all recent packets, and remove all older than FLOOD_EXPIRE_TIME + */ +void PacketHistory::clearExpiredRecentPackets() +{ + LOG_DEBUG("recentPackets size=%ld", recentPackets.size()); + + for (auto it = recentPackets.begin(); it != recentPackets.end();) { + if (!Throttle::isWithinTimespanMs(it->rxTimeMsec, FLOOD_EXPIRE_TIME)) { + it = recentPackets.erase(it); // erase returns iterator pointing to element immediately following the one erased + } else { + ++it; + } + } + + LOG_DEBUG("recentPackets size=%ld (after clearing expired packets)", recentPackets.size()); +} \ No newline at end of file diff --git a/src/mesh/PacketHistory.h b/src/mesh/PacketHistory.h new file mode 100644 index 0000000..89d237a --- /dev/null +++ b/src/mesh/PacketHistory.h @@ -0,0 +1,45 @@ +#pragma once + +#include "Router.h" +#include + +/// We clear our old flood record 10 minutes after we see the last of it +#define FLOOD_EXPIRE_TIME (10 * 60 * 1000L) + +/** + * A record of a recent message broadcast + */ +struct PacketRecord { + NodeNum sender; + PacketId id; + uint32_t rxTimeMsec; // Unix time in msecs - the time we received it + + bool operator==(const PacketRecord &p) const { return sender == p.sender && id == p.id; } +}; + +class PacketRecordHashFunction +{ + public: + size_t operator()(const PacketRecord &p) const { return (std::hash()(p.sender)) ^ (std::hash()(p.id)); } +}; + +/** + * This is a mixin that adds a record of past packets we have seen + */ +class PacketHistory +{ + private: + std::unordered_set recentPackets; + + void clearExpiredRecentPackets(); // clear all recentPackets older than FLOOD_EXPIRE_TIME + + public: + 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 + */ + bool wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpdate = true); +}; diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp new file mode 100644 index 0000000..98db38c --- /dev/null +++ b/src/mesh/PhoneAPI.cpp @@ -0,0 +1,639 @@ +#include "configuration.h" +#if !MESHTASTIC_EXCLUDE_GPS +#include "GPS.h" +#endif + +#include "Channels.h" +#include "Default.h" +#include "FSCommon.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "PacketHistory.h" +#include "PhoneAPI.h" +#include "PowerFSM.h" +#include "RadioInterface.h" +#include "TypeConversions.h" +#include "main.h" +#include "xmodem.h" + +#if FromRadio_size > MAX_TO_FROM_RADIO_SIZE +#error FromRadio is too big +#endif + +#if ToRadio_size > MAX_TO_FROM_RADIO_SIZE +#error ToRadio is too big +#endif +#if !MESHTASTIC_EXCLUDE_MQTT +#include "mqtt/MQTT.h" +#endif +#include "Throttle.h" +#include + +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 + } + + // even if we were already connected - restart our state machine + state = STATE_SEND_MY_INFO; + pauseBluetoothLogging = true; + filesManifest = getFiles("/", 10); + LOG_DEBUG("Got %d files in manifest", filesManifest.size()); + + LOG_INFO("Starting API client config"); + nodeInfoForPhone.num = 0; // Don't keep returning old nodeinfos + resetReadIndex(); +} + +void PhoneAPI::close() +{ + LOG_INFO("PhoneAPI::close()"); + + 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 = {}; + nodeInfoForPhone = {}; + packetForPhone = NULL; + filesManifest.clear(); + fromRadioNum = 0; + config_nonce = 0; + config_state = 0; + pauseBluetoothLogging = 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(); + + 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("Disconnecting from phone"); + close(); + break; + case meshtastic_ToRadio_xmodemPacket_tag: + LOG_INFO("Got xmodem packet"); +#ifdef FSCom + xModem.handlePacket(toRadioScratch.xmodemPacket); +#endif + break; +#if !MESHTASTIC_EXCLUDE_MQTT + case meshtastic_ToRadio_mqttClientProxyMessage_tag: + LOG_INFO("Got MqttClientProxy message"); + 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"); + break; + default: + // Ignore nop messages + // LOG_DEBUG("Error: unexpected ToRadio variant"); + break; + } + } else { + LOG_ERROR("Error: ignoring malformed toradio"); + } + + return false; +} + +/** + * Get the next packet we want to send to the phone, or NULL if no such packet is available. + * + * We assume buf is at least FromRadio_size bytes long. + * + * Our sending states progress in the following sequence (the client apps ASSUME THIS SEQUENCE, DO NOT CHANGE IT): + STATE_SEND_MY_INFO, // send our my info record + STATE_SEND_OWN_NODEINFO, + STATE_SEND_METADATA, + STATE_SEND_CHANNELS + STATE_SEND_CONFIG, + STATE_SEND_MODULE_CONFIG, + STATE_SEND_OTHER_NODEINFOS, // states progress in this order as the device sends to the client + STATE_SEND_FILEMANIFEST, + STATE_SEND_COMPLETE_ID, + STATE_SEND_PACKETS // send packets or debug strings + */ + +size_t PhoneAPI::getFromRadio(uint8_t *buf) +{ + if (!available()) { + // LOG_DEBUG("getFromRadio=not 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_INFO("getFromRadio=STATE_SEND_NOTHING"); + break; + + case STATE_SEND_MY_INFO: + LOG_INFO("getFromRadio=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; + fromRadioScratch.my_info = myNodeInfo; + state = STATE_SEND_OWN_NODEINFO; + + service->refreshLocalMeshNode(); // Update my NodeInfo because the client will be asking for it soon. + break; + + case STATE_SEND_OWN_NODEINFO: { + LOG_INFO("getFromRadio=STATE_SEND_OWN_NODEINFO"); + auto us = nodeDB->readNextMeshNode(readIndex); + if (us) { + nodeInfoForPhone = TypeConversions::ConvertToNodeInfo(us); + nodeInfoForPhone.has_hops_away = false; + nodeInfoForPhone.is_favorite = true; + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag; + fromRadioScratch.node_info = nodeInfoForPhone; + // Should allow us to resume sending NodeInfo in STATE_SEND_OTHER_NODEINFOS + nodeInfoForPhone.num = 0; + } + state = STATE_SEND_METADATA; + break; + } + + case STATE_SEND_METADATA: + LOG_INFO("getFromRadio=STATE_SEND_METADATA"); + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_metadata_tag; + fromRadioScratch.metadata = getDeviceMetadata(); + state = STATE_SEND_CHANNELS; + break; + + case STATE_SEND_CHANNELS: + LOG_INFO("getFromRadio=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) { + state = STATE_SEND_CONFIG; + config_state = _meshtastic_AdminMessage_ConfigType_MIN + 1; + } + break; + + case STATE_SEND_CONFIG: + LOG_INFO("getFromRadio=STATE_SEND_CONFIG"); + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_tag; + switch (config_state) { + case meshtastic_Config_device_tag: + fromRadioScratch.config.which_payload_variant = meshtastic_Config_device_tag; + fromRadioScratch.config.payload_variant.device = config.device; + break; + case meshtastic_Config_position_tag: + fromRadioScratch.config.which_payload_variant = meshtastic_Config_position_tag; + fromRadioScratch.config.payload_variant.position = config.position; + break; + case meshtastic_Config_power_tag: + 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: + fromRadioScratch.config.which_payload_variant = meshtastic_Config_network_tag; + fromRadioScratch.config.payload_variant.network = config.network; + break; + case meshtastic_Config_display_tag: + fromRadioScratch.config.which_payload_variant = meshtastic_Config_display_tag; + fromRadioScratch.config.payload_variant.display = config.display; + break; + case meshtastic_Config_lora_tag: + fromRadioScratch.config.which_payload_variant = meshtastic_Config_lora_tag; + fromRadioScratch.config.payload_variant.lora = config.lora; + break; + case meshtastic_Config_bluetooth_tag: + fromRadioScratch.config.which_payload_variant = meshtastic_Config_bluetooth_tag; + fromRadioScratch.config.payload_variant.bluetooth = config.bluetooth; + break; + case meshtastic_Config_security_tag: + fromRadioScratch.config.which_payload_variant = meshtastic_Config_security_tag; + fromRadioScratch.config.payload_variant.security = config.security; + break; + case meshtastic_Config_sessionkey_tag: + fromRadioScratch.config.which_payload_variant = meshtastic_Config_sessionkey_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: + LOG_INFO("getFromRadio=STATE_SEND_MODULECONFIG"); + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_moduleConfig_tag; + switch (config_state) { + case meshtastic_ModuleConfig_mqtt_tag: + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_mqtt_tag; + fromRadioScratch.moduleConfig.payload_variant.mqtt = moduleConfig.mqtt; + break; + case meshtastic_ModuleConfig_serial_tag: + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_serial_tag; + fromRadioScratch.moduleConfig.payload_variant.serial = moduleConfig.serial; + break; + case meshtastic_ModuleConfig_external_notification_tag: + 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: + 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: + 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: + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_telemetry_tag; + fromRadioScratch.moduleConfig.payload_variant.telemetry = moduleConfig.telemetry; + break; + case meshtastic_ModuleConfig_canned_message_tag: + 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: + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_audio_tag; + fromRadioScratch.moduleConfig.payload_variant.audio = moduleConfig.audio; + break; + case meshtastic_ModuleConfig_remote_hardware_tag: + 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: + 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: + 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: + 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: + 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)) { + // Clients sending special nonce don't want to see other nodeinfos + state = config_nonce == SPECIAL_NONCE ? STATE_SEND_FILEMANIFEST : STATE_SEND_OTHER_NODEINFOS; + config_state = 0; + } + break; + + case STATE_SEND_OTHER_NODEINFOS: { + LOG_INFO("getFromRadio=STATE_SEND_OTHER_NODEINFOS"); + if (nodeInfoForPhone.num != 0) { + LOG_INFO("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", nodeInfoForPhone.num, nodeInfoForPhone.last_heard, + nodeInfoForPhone.user.id, nodeInfoForPhone.user.long_name); + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag; + fromRadioScratch.node_info = nodeInfoForPhone; + // Stay in current state until done sending nodeinfos + nodeInfoForPhone.num = 0; // We just consumed a nodeinfo, will need a new one next time + } else { + LOG_INFO("Done sending nodeinfos"); + state = STATE_SEND_FILEMANIFEST; + // Go ahead and send that ID right now + return getFromRadio(buf); + } + break; + } + + case STATE_SEND_FILEMANIFEST: { + LOG_INFO("getFromRadio=STATE_SEND_FILEMANIFEST"); + // last element + if (config_state == filesManifest.size()) { // 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_INFO("getFromRadio=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) + // LOG_DEBUG("encoding toPhone packet to phone variant=%d, %d bytes", fromRadioScratch.which_payload_variant, numbytes); + return numbytes; + } + + LOG_DEBUG("no FromRadio packet available"); + return 0; +} + +void PhoneAPI::sendConfigComplete() +{ + LOG_INFO("getFromRadio=STATE_SEND_COMPLETE_ID"); + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_complete_id_tag; + fromRadioScratch.config_complete_id = config_nonce; + config_nonce = 0; + state = STATE_SEND_PACKETS; + 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::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_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: + if (nodeInfoForPhone.num == 0) { + auto nextNode = nodeDB->readNextMeshNode(readIndex); + if (nextNode) { + nodeInfoForPhone = TypeConversions::ConvertToNodeInfo(nextNode); + nodeInfoForPhone.hops_away = nodeInfoForPhone.num == nodeDB->getNodeNum() ? 0 : nodeInfoForPhone.hops_away; + nodeInfoForPhone.is_favorite = + nodeInfoForPhone.is_favorite || nodeInfoForPhone.num == nodeDB->getNodeNum(); // Our node is always a favorite + } + } + return true; // Always say we have something, because we might need to advance our state machine + 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; + } +#endif + +#ifdef ARCH_ESP32 +#if !MESHTASTIC_EXCLUDE_STOREFORWARD + // Check if StoreForward has packets stored for us. + if (!packetForPhone && storeForwardModule) + packetForPhone = storeForwardModule->getForPhone(); +#endif +#endif + + if (!packetForPhone) + packetForPhone = service->getForPhone(); + hasPacket = !!packetForPhone; + // LOG_DEBUG("available hasPacket=%d", hasPacket); + return hasPacket; + } + default: + LOG_ERROR("PhoneAPI::available unexpected state %d", state); + } + + 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); +} + +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 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); + + if (p.id > 0 && wasSeenRecently(p.id)) { + LOG_DEBUG("Ignoring 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 limiting 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_POSITION_APP && lastPortNumToRadio[p.decoded.portnum] && + Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], FIVE_SECONDS_MS)) { + LOG_WARN("Rate limiting 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; + } + 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) + + if (state == STATE_SEND_PACKETS) { + LOG_INFO("Telling client we have new packets %u", newValue); + onNowHasData(newValue); + } else { + LOG_DEBUG("(Client not yet interested in packets)"); + } + + return timeout ? -1 : 0; // If we timed out, MeshService should stop iterating through observers as we just removed one +} \ No newline at end of file diff --git a/src/mesh/PhoneAPI.h b/src/mesh/PhoneAPI.h new file mode 100644 index 0000000..3247fee --- /dev/null +++ b/src/mesh/PhoneAPI.h @@ -0,0 +1,173 @@ +#pragma once + +#include "Observer.h" +#include "mesh-pb-constants.h" +#include "meshtastic/portnums.pb.h" +#include +#include +#include +#include + +// Make sure that we never let our packets grow too large for one BLE packet +#define MAX_TO_FROM_RADIO_SIZE 512 + +#if meshtastic_FromRadio_size > MAX_TO_FROM_RADIO_SIZE +#error "meshtastic_FromRadio_size is too large for our BLE packets" +#endif +#if meshtastic_ToRadio_size > MAX_TO_FROM_RADIO_SIZE +#error "meshtastic_ToRadio_size is too large for our BLE packets" +#endif + +#define SPECIAL_NONCE 69420 + +/** + * Provides our protobuf based API which phone/PC clients can use to talk to our device + * over UDP, bluetooth or serial. + * + * Subclass to customize behavior for particular type of transport (BLE, UDP, TCP, serial) + * + * 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 +{ + enum State { + STATE_SEND_NOTHING, // Initial state, don't send anything until the client starts asking for config + 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; + + 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 + + /** + * 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; + + // 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 MqttClientProxyMessage packet just as packetForPhone + meshtastic_MqttClientProxyMessage *mqttClientProxyMessageForPhone = 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; + + 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; + + std::vector filesManifest = {}; + + void resetReadIndex() { readIndex = 0; } + + public: + 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(); + + /** + * 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); + + /** + * 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(); + + /** + * Return true if we have data available to send to the phone + */ + bool available(); + + bool isConnected() { return state != STATE_SEND_NOTHING; } + + 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; + + /// 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(); + + /// 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) {} + + private: + void releasePhonePacket(); + + void releaseQueueStatusPhonePacket(); + + void releaseMqttClientProxyPhonePacket(); + + void releaseClientNotification(); + + /// begin a new connection + void handleStartConfig(); + + 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); + + /// If the mesh service tells us fromNum has changed, tell the phone + virtual int onNotify(uint32_t newValue) override; +}; \ No newline at end of file diff --git a/src/mesh/PointerQueue.h b/src/mesh/PointerQueue.h new file mode 100644 index 0000000..b45245e --- /dev/null +++ b/src/mesh/PointerQueue.h @@ -0,0 +1,30 @@ +#pragma once + +#include "TypedQueue.h" + +/** + * A wrapper for freertos queues that assumes each element is a pointer + */ +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; + + 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; + + return this->dequeueFromISR(&p, higherPriWoken) ? p : nullptr; + } +#endif +}; diff --git a/src/mesh/ProtobufModule.cpp b/src/mesh/ProtobufModule.cpp new file mode 100644 index 0000000..ef18c0e --- /dev/null +++ b/src/mesh/ProtobufModule.cpp @@ -0,0 +1,2 @@ +#include "ProtobufModule.h" +#include "configuration.h" diff --git a/src/mesh/ProtobufModule.h b/src/mesh/ProtobufModule.h new file mode 100644 index 0000000..4ddac9a --- /dev/null +++ b/src/mesh/ProtobufModule.h @@ -0,0 +1,123 @@ +#pragma once +#include "SinglePortModule.h" + +/** + * A base class for mesh modules that assume that they are sending/receiving one particular protobuf based + * payload. Using one particular app ID. + * + * 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; + + public: + uint8_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 protobuf 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 protobuf 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 new file mode 100644 index 0000000..db56659 --- /dev/null +++ b/src/mesh/RF95Interface.cpp @@ -0,0 +1,340 @@ +#if RADIOLIB_EXCLUDE_SX127X != 1 +#include "RF95Interface.h" +#include "MeshRadio.h" // kinda yucky, but we need to know which region we are in +#include "RadioLibRF95.h" +#include "configuration.h" +#include "error.h" + +#if ARCH_PORTDUINO +#include "PortduinoGlue.h" +#endif + +#ifndef RF95_MAX_POWER +#define RF95_MAX_POWER 20 +#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. + +#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; +} 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; +} + +// 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); + } + } + + // 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); +} + +/** Some boards require GPIO control of tx vs rx paths */ +void RF95Interface::setTransmitEnable(bool txon) +{ +#ifdef RF95_TXEN + digitalWrite(RF95_TXEN, txon ? 1 : 0); +#elif ARCH_PORTDUINO + if (settingsMap[txen] != RADIOLIB_NC) { + digitalWrite(settingsMap[txen], txon ? 1 : 0); + } +#endif + +#ifdef RF95_RXEN + digitalWrite(RF95_RXEN, txon ? 0 : 1); +#elif ARCH_PORTDUINO + if (settingsMap[rxen] != RADIOLIB_NC) { + digitalWrite(settingsMap[rxen], 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(); + +#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; +#endif + + if (power > RF95_MAX_POWER) // This chip has lower power limits than some + power = RF95_MAX_POWER; + + limitPower(); + + iface = lora = new RadioLibRF95(&module); + +#ifdef RF95_TCXO + pinMode(RF95_TCXO, OUTPUT); + digitalWrite(RF95_TCXO, 1); +#endif + + // 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); +#else + // 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) + */ + +#ifdef RF95_TXEN + pinMode(RF95_TXEN, OUTPUT); + digitalWrite(RF95_TXEN, 0); +#endif + +#ifdef RF95_FAN_EN + pinMode(RF95_FAN_EN, OUTPUT); + digitalWrite(RF95_FAN_EN, 1); +#endif + +#ifdef RF95_RXEN + pinMode(RF95_RXEN, OUTPUT); + digitalWrite(RF95_RXEN, 1); +#endif +#if ARCH_PORTDUINO + if (settingsMap[txen] != RADIOLIB_NC) { + pinMode(settingsMap[txen], OUTPUT); + digitalWrite(settingsMap[txen], 0); + } + if (settingsMap[rxen] != RADIOLIB_NC) { + pinMode(settingsMap[rxen], OUTPUT); + digitalWrite(settingsMap[rxen], 0); + } +#endif + 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); +#if defined(RADIOMASTER_900_BANDIT_NANO) || defined(RADIOMASTER_900_BANDIT) + 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) + startReceive(); // start receiving + + return res == RADIOLIB_ERR_NONE; +} + +void INTERRUPT_ATTR RF95Interface::disableInterrupt() +{ + lora->clearDio0Action(); +} + +bool RF95Interface::reconfigure() +{ + RadioLibInterface::reconfigure(); + + // 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); + + 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->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->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); + + 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); +#else + err = lora->setOutputPower(power); +#endif + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + + startReceive(); // restart receiving + + 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()); +} + +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(); +} + +/** We override to turn on transmitter power as needed. + */ +void RF95Interface::configHardwareForSend() +{ + setTransmitEnable(true); + + 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); + + isReceiving = true; + + // 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(); + + 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; +} + +/** Could we send right now (i.e. either not actively receiving or transmitting)? */ +bool RF95Interface::isActivelyReceiving() +{ + return lora->isReceiving(); +} + +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); +#endif + + return true; +} +#endif \ No newline at end of file diff --git a/src/mesh/RF95Interface.h b/src/mesh/RF95Interface.h new file mode 100644 index 0000000..327e579 --- /dev/null +++ b/src/mesh/RF95Interface.h @@ -0,0 +1,72 @@ +#pragma once +#if RADIOLIB_EXCLUDE_SX127X != 1 +#include "MeshRadio.h" // kinda yucky, but we need to know which region we are in +#include "RadioLibInterface.h" +#include "RadioLibRF95.h" + +/** + * 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 + + 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; } + + /// 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; + + /// 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; + + /** + * 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; + + /** 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; + + /** + * Add SNR data to received messages + */ + virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) override; + + virtual void setStandby() override; + + /** + * We override to turn on transmitter power as needed. + */ + virtual void configHardwareForSend() override; + + 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/RadioInterface.cpp b/src/mesh/RadioInterface.cpp new file mode 100644 index 0000000..f51c9bc --- /dev/null +++ b/src/mesh/RadioInterface.cpp @@ -0,0 +1,627 @@ +#include "RadioInterface.h" +#include "Channels.h" +#include "DisplayFormatters.h" +#include "MeshRadio.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "Router.h" +#include "configuration.h" +#include "main.h" +#include "sleep.h" +#include +#include +#include + +#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[] = { + /* + https://link.springer.com/content/pdf/bbm%3A978-1-4842-4357-2%2F1.pdf + https://www.thethingsnetwork.org/docs/lorawan/regional-parameters/ + */ + RDEF(US, 902.0f, 928.0f, 100, 0, 30, true, false, false), + + /* + https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf + */ + RDEF(EU_433, 433.0f, 434.0f, 10, 0, 12, true, false, false), + + /* + https://www.thethingsnetwork.org/docs/lorawan/duty-cycle/ + https://www.thethingsnetwork.org/docs/lorawan/regional-parameters/ + https://www.legislation.gov.uk/uksi/1999/930/schedule/6/part/III/made/data.xht?view=snippet&wrap=true + + audio_permitted = false per regulation + + Special Note: + The link above describes LoRaWAN's band plan, stating a power limit of 16 dBm. This is their own suggested specification, + we do not need to follow it. The European Union regulations clearly state that the power limit for this frequency range is + 500 mW, or 27 dBm. It also states that we can use interference avoidance and spectrum access techniques (such as LBT + + AFA) to avoid a duty cycle. (Please refer to line P page 22 of this document.) + https://www.etsi.org/deliver/etsi_en/300200_300299/30022002/03.01.01_60/en_30022002v030101p.pdf + */ + RDEF(EU_868, 869.4f, 869.65f, 10, 0, 27, false, false, false), + + /* + https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf + */ + RDEF(CN, 470.0f, 510.0f, 100, 0, 19, true, false, false), + + /* + https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf + https://www.arib.or.jp/english/html/overview/doc/5-STD-T108v1_5-E1.pdf + https://qiita.com/ammo0613/items/d952154f1195b64dc29f + */ + RDEF(JP, 920.5f, 923.5f, 100, 0, 13, true, false, false), + + /* + https://www.iot.org.au/wp/wp-content/uploads/2016/12/IoTSpectrumFactSheet.pdf + https://iotalliance.org.nz/wp-content/uploads/sites/4/2019/05/IoT-Spectrum-in-NZ-Briefing-Paper.pdf + */ + RDEF(ANZ, 915.0f, 928.0f, 100, 0, 30, true, false, false), + + /* + https://digital.gov.ru/uploaded/files/prilozhenie-12-k-reshenyu-gkrch-18-46-03-1.pdf + + Note: + - We do LBT, so 100% is allowed. + */ + RDEF(RU, 868.7f, 869.2f, 100, 0, 20, true, false, false), + + /* + ??? + */ + RDEF(KR, 920.0f, 923.0f, 100, 0, 0, true, false, false), + + /* + Taiwan, 920-925Mhz, limited to 0.5W indoor or coastal, 1.0W outdoor. + 5.8.1 in the Low-power Radio-frequency Devices Technical Regulations + https://www.ncc.gov.tw/english/files/23070/102_5190_230703_1_doc_C.PDF + https://gazette.nat.gov.tw/egFront/e_detail.do?metaid=147283 + */ + RDEF(TW, 920.0f, 925.0f, 100, 0, 27, true, false, false), + + /* + https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf + */ + RDEF(IN, 865.0f, 867.0f, 100, 0, 30, true, false, false), + + /* + https://rrf.rsm.govt.nz/smart-web/smart/page/-smart/domain/licence/LicenceSummary.wdk?id=219752 + https://iotalliance.org.nz/wp-content/uploads/sites/4/2019/05/IoT-Spectrum-in-NZ-Briefing-Paper.pdf + */ + RDEF(NZ_865, 864.0f, 868.0f, 100, 0, 36, true, false, false), + + /* + https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf + */ + RDEF(TH, 920.0f, 925.0f, 100, 0, 16, true, false, false), + + /* + 433,05-434,7 Mhz 10 mW + https://nkrzi.gov.ua/images/upload/256/5810/PDF_UUZ_19_01_2016.pdf + */ + RDEF(UA_433, 433.0f, 434.7f, 10, 0, 10, true, false, false), + + /* + 868,0-868,6 Mhz 25 mW + https://nkrzi.gov.ua/images/upload/256/5810/PDF_UUZ_19_01_2016.pdf + */ + RDEF(UA_868, 868.0f, 868.6f, 1, 0, 14, true, false, false), + + /* + Malaysia + 433 - 435 MHz at 100mW, no restrictions. + https://www.mcmc.gov.my/skmmgovmy/media/General/pdf/Short-Range-Devices-Specification.pdf + */ + RDEF(MY_433, 433.0f, 435.0f, 100, 0, 20, true, false, false), + + /* + Malaysia + 919 - 923 Mhz at 500mW, no restrictions. + 923 - 924 MHz at 500mW with 1% duty cycle OR frequency hopping. + Frequency hopping is used for 919 - 923 MHz. + https://www.mcmc.gov.my/skmmgovmy/media/General/pdf/Short-Range-Devices-Specification.pdf + */ + RDEF(MY_919, 919.0f, 924.0f, 100, 0, 27, true, true, false), + + /* + Singapore + SG_923 Band 30d: 917 - 925 MHz at 100mW, no restrictions. + https://www.imda.gov.sg/-/media/imda/files/regulation-licensing-and-consultations/ict-standards/telecommunication-standards/radio-comms/imdatssrd.pdf + */ + RDEF(SG_923, 917.0f, 925.0f, 100, 0, 20, true, false, false), + + /* + Philippines + 433 - 434.7 MHz <10 mW erp, NTC approved device required + 868 - 869.4 MHz <25 mW erp, NTC approved device required + 915 - 918 MHz <250 mW EIRP, no external antennna allowed + https://github.com/meshtastic/firmware/issues/4948#issuecomment-2394926135 + */ + + RDEF(PH_433, 433.0f, 434.7f, 100, 0, 10, true, false, false), RDEF(PH_868, 868.0f, 869.4f, 100, 0, 14, true, false, false), + RDEF(PH_915, 915.0f, 918.0f, 100, 0, 24, true, false, false), + + /* + 2.4 GHZ WLAN Band equivalent. Only for SX128x chips. + */ + RDEF(LORA_24, 2400.0f, 2483.5f, 100, 0, 10, true, false, true), + + /* + This needs to be last. Same as US. + */ + RDEF(UNSET, 902.0f, 928.0f, 100, 0, 30, true, false, false) + +}; + +const RegionInfo *myRegion; +bool RadioInterface::uses_default_frequency_slot = true; + +static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1]; + +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); +#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); +#endif + myRegion = r; +} + +/** + * ## LoRaWAN for North America + +LoRaWAN defines 64, 125 kHz channels from 902.3 to 914.9 MHz increments. + +The maximum output power for North America is +30 dBM. + +The band is from 902 to 928 MHz. It mentions channel number and its respective channel frequency. All the 13 channels are +separated by 2.16 MHz with respect to the adjacent channels. Channel zero starts at 903.08 MHz center frequency. +*/ + +/** + * 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 RadioInterface::getPacketTime(uint32_t pl) +{ + 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 + + 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 RadioInterface::getPacketTime(const meshtastic_MeshPacket *p) +{ + 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); +} + +/** 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(2, CWsize) + 2 * CWmax + pow(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(2, CWsize)) * slotTimeMsec; +} + +/** The delay to use when we want to flood a message */ +uint32_t RadioInterface::getTxDelayMsecWeighted(float snr) +{ + // The minimum value for a LoRa SNR + const uint32_t SNR_MIN = -20; + + // The maximum value for a LoRa SNR + const uint32_t SNR_MAX = 15; + + // high SNR = large CW size (Long Delay) + // low SNR = small CW size (Short Delay) + uint32_t delay = 0; + uint8_t CWsize = map(snr, SNR_MIN, SNR_MAX, CWmin, CWmax); + // LOG_DEBUG("rx_snr of %f so setting CWsize to:%d", snr, CWsize); + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) { + delay = random(0, 2 * CWsize) * slotTimeMsec; + LOG_DEBUG("rx_snr found in packet. As a router, setting tx delay:%d", delay); + } else { + // offset the maximum delay for routers: (2 * CWmax * slotTimeMsec) + delay = (2 * CWmax * slotTimeMsec) + random(0, pow(2, CWsize)) * slotTimeMsec; + LOG_DEBUG("rx_snr found in packet. Setting tx delay:%d", delay); + } + + return delay; +} + +void printPacket(const char *prefix, const meshtastic_MeshPacket *p) +{ +#ifdef DEBUG_PORT + std::string out = DEBUG_PORT.mt_sprintf("%s (id=0x%08x fr=0x%02x to=0x%02x, WantAck=%d, HopLim=%d Ch=0x%x", prefix, p->id, + p->from & 0xff, p->to & 0xff, 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); + + if (s.want_response) + out += DEBUG_PORT.mt_sprintf(" WANTRESP"); + + 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.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); + + /* 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"; + } + + 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->priority != 0) + out += DEBUG_PORT.mt_sprintf(" priority=%d", p->priority); + + out += ")"; + LOG_DEBUG("%s", out.c_str()); +#endif +} + +RadioInterface::RadioInterface() +{ + assert(sizeof(PacketHeader) == MESHTASTIC_HEADER_LENGTH); // make sure the compiler did what we expected +} + +bool RadioInterface::reconfigure() +{ + applyModemConfig(); + return true; +} + +bool RadioInterface::init() +{ + LOG_INFO("Starting meshradio init..."); + + 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. + + applyModemConfig(); + + return true; +} + +int RadioInterface::notifyDeepSleepCb(void *unused) +{ + sleep(); + return 0; +} + +/** hash a string into an integer + * + * djb2 by Dan Bernstein. + * http://www.cse.yorku.ca/~oz/hash.html + */ +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 */ + + return hash; +} + +/** + * Save our frequency for later reuse. + */ +void RadioInterface::saveFreq(float freq) +{ + savedFreq = freq; +} + +/** + * Save our channel for later reuse. + */ +void RadioInterface::saveChannelNum(uint32_t channel_num) +{ + savedChannelNum = channel_num; +} + +/** + * Save our frequency for later reuse. + */ +float RadioInterface::getFreq() +{ + return savedFreq; +} + +/** + * Save our channel for later reuse. + */ +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) { + + 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; + 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; + case meshtastic_Config_LoRaConfig_ModemPreset_VERY_LONG_SLOW: + bw = (myRegion->wideLora) ? 203.125 : 62.5; + cr = 8; + sf = 12; + break; + } + } 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) { + static const char *err_string = "Regional frequency range is smaller than bandwidth. Falling back to default preset."; + LOG_ERROR(err_string); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_ERROR; + sprintf(cn->message, 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; + } + } + + power = loraConfig.tx_power; + + if ((power == 0) || ((power + REGULATORY_GAIN_LORA > myRegion->powerLimit) && !devicestate.owner.is_licensed)) + power = myRegion->powerLimit - REGULATORY_GAIN_LORA; + + 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)) % 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(bw, sf); + preambleTimeMsec = getPacketTime((uint32_t)0); + maxPacketTimeMsec = getPacketTime(meshtastic_Constants_DATA_PAYLOAD_LEN + sizeof(PacketHeader)); + + 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", slotTimeMsec); +} + +/** + * 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() +{ + uint8_t maxPower = 255; // No limit + + if (myRegion->powerLimit) + maxPower = myRegion->powerLimit; + + if ((power > maxPower) && !devicestate.owner.is_licensed) { + LOG_INFO("Lowering transmit power because of regulatory limits"); + power = maxPower; + } + + LOG_INFO("Set radio: final power level=%d", power); +} + +void RadioInterface::deliverToReceiver(meshtastic_MeshPacket *p) +{ + if (router) + 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); + + // LOG_DEBUG("sending 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 + + lastTxStart = millis(); + + radioBuffer.header.from = p->from; + radioBuffer.header.to = p->to; + radioBuffer.header.id = p->id; + radioBuffer.header.channel = p->channel; + radioBuffer.header.next_hop = 0; // *** For future use *** + radioBuffer.header.relay_node = 0; // *** For future use *** + 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); + + memcpy(radioBuffer.payload, p->encrypted.bytes, p->encrypted.size); + + sendingPacket = p; + return p->encrypted.size + sizeof(PacketHeader); +} diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h new file mode 100644 index 0000000..89a4c70 --- /dev/null +++ b/src/mesh/RadioInterface.h @@ -0,0 +1,256 @@ +#pragma once + +#include "MemoryPool.h" +#include "MeshTypes.h" +#include "Observer.h" +#include "PointerQueue.h" +#include "airtime.h" +#include "error.h" + +#define MAX_TX_QUEUE 16 // max number of packets which can be waiting for transmission + +#define MAX_LORA_PAYLOAD_LEN 255 // max length of 255 per Semtech's datasheets on SX12xx +#define MESHTASTIC_HEADER_LENGTH 16 +#define MESHTASTIC_PKC_OVERHEAD 12 + +#define PACKET_FLAGS_HOP_LIMIT_MASK 0x07 +#define PACKET_FLAGS_WANT_ACK_MASK 0x08 +#define PACKET_FLAGS_VIA_MQTT_MASK 0x10 +#define PACKET_FLAGS_HOP_START_MASK 0xE0 +#define PACKET_FLAGS_HOP_START_SHIFT 5 + +/** + * This structure has to exactly match the wire layout when sent over the radio link. Used to keep compatibility + * with the old radiohead implementation. + */ +typedef struct { + NodeNum to, from; // can be 1 byte or four 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; + + /** The channel hash - used as a hint for the decoder to limit which channels we consider */ + uint8_t channel; + + // ***For future use*** Last byte of the NodeNum of the next-hop for this packet + uint8_t next_hop; + + // ***For future use*** Last byte of the NodeNum of the node that will relay/relayed this packet + uint8_t relay_node; +} PacketHeader; + +/** + * This structure represent the structured buffer : a PacketHeader then the payload. The whole is + * MAX_LORA_PAYLOAD_LEN + 1 length + * 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 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; + +/** + * Basic operations all radio chipsets must implement. + * + * 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 + + CallbackObserver configChangedObserver = + CallbackObserver(this, &RadioInterface::reloadConfig); + + CallbackObserver preflightSleepObserver = + CallbackObserver(this, &RadioInterface::preflightSleepCb); + + CallbackObserver notifyDeepSleepObserver = + CallbackObserver(this, &RadioInterface::notifyDeepSleepCb); + + protected: + bool disabled = false; + + float bw = 125; + uint8_t sf = 9; + uint8_t cr = 5; + /** Slottime is the minimum time to wait, consisting of: + - CAD duration (maximum of SX126x and SX127x); + - 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 slotTimeMsec = computeSlotTimeMsec(bw, sf); + 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 + uint32_t maxPacketTimeMsec = 3246; // 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 = 2; // minimum CWsize + const uint8_t CWmax = 7; // maximum CWsize + + meshtastic_MeshPacket *sendingPacket = NULL; // The packet we are currently sending + uint32_t lastTxStart = 0L; + + uint32_t computeSlotTimeMsec(float bw, float sf) { return 8.5 * pow(2, sf) / bw + 0.2 + 0.4 + 7; } + + /** + * 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(); + + 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; } + + 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; } + + /// 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; + + /** 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; } + + // 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(); + + /// 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 when we want to send something */ + uint32_t getTxDelayMsec(); + + /** The delay to use when we want to flood a message. Use a weighted scale based on SNR */ + uint32_t getTxDelayMsecWeighted(float snr); + + /** + * 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); + uint32_t getPacketTime(uint32_t totalPacketLen); + + /** + * Get the channel we saved. + */ + uint32_t getChannelNum(); + + /** + * 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; } + + // 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() + + 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); + + /** + * Some regulatory regions limit xmit power. + * This function should be called by subclasses after setting their desired power. It might lower it + */ + void limitPower(); + + /** + * 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); + + 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; } + + int notifyDeepSleepCb(void *unused = NULL); + + int reloadConfig(void *unused) + { + reconfigure(); + return 0; + } +}; + +/// Debug printing for packets +void printPacket(const char *prefix, const meshtastic_MeshPacket *p); \ No newline at end of file diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp new file mode 100644 index 0000000..9bf1f27 --- /dev/null +++ b/src/mesh/RadioLibInterface.cpp @@ -0,0 +1,496 @@ +#include "RadioLibInterface.h" +#include "MeshTypes.h" +#include "NodeDB.h" +#include "PowerMon.h" +#include "SPILock.h" +#include "Throttle.h" +#include "configuration.h" +#include "error.h" +#include "main.h" +#include "mesh-pb-constants.h" +#include +#include + +void LockingArduinoHal::spiBeginTransaction() +{ + spiLock->lock(); + + ArduinoHal::spiBeginTransaction(); +} + +void LockingArduinoHal::spiEndTransaction() +{ + ArduinoHal::spiEndTransaction(); + + spiLock->unlock(); +} +#if ARCH_PORTDUINO +void LockingArduinoHal::spiTransfer(uint8_t *out, size_t len, uint8_t *in) +{ + if (busy == RADIOLIB_NC) { + spi->transfer(out, in, len); + } else { + uint16_t offset = 0; + + while (len) { + uint8_t block_size = (len < 20 ? len : 20); + spi->transfer((out != NULL ? out + offset : NULL), (in != NULL ? in + offset : NULL), block_size); + if (block_size == len) + return; + + // ensure GPIO is low + + uint32_t start = millis(); + while (digitalRead(busy)) { + if (!Throttle::isWithinTimespanMs(start, 2000)) { + LOG_ERROR("GPIO mid-transfer timeout, is it connected?"); + return; + } + } + + offset += block_size; + len -= block_size; + } + } +} +#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; +#if defined(ARCH_STM32WL) && defined(USE_SX1262) + module.setCb_digitalWrite(stm32wl_emulate_digitalWrite); + module.setCb_digitalRead(stm32wl_emulate_digitalRead); +#endif +} + +#ifdef ARCH_ESP32 +// ESP32 doesn't use that flag +#define YIELD_FROM_ISR(x) portYIELD_FROM_ISR() +#else +#define YIELD_FROM_ISR(x) portYIELD_FROM_ISR(x) +#endif + +void INTERRUPT_ATTR RadioLibInterface::isrLevel0Common(PendingISR cause) +{ + instance->disableInterrupt(); + + 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); +} + +void INTERRUPT_ATTR RadioLibInterface::isrRxLevel0() +{ + isrLevel0Common(ISR_RX); +} + +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(); + + 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) && !(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 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; +} + +/// 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) +{ + +#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 because RegionCode_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 + + // Sometimes when testing it is useful to be able to never turn on the xmitter +#ifndef LORA_DISABLE_SENDING + printPacket("enqueuing for send", p); + + LOG_DEBUG("txGood=%d,txRelay=%d,rxGood=%d,rxBad=%d", txGood, txRelay, rxGood, rxBad); + ErrorCode res = txQueue.enqueue(p) ? ERRNO_OK : ERRNO_UNKNOWN; + + 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 transmitting."); + setTransmitDelay(); + + return res; +#else + packetPool.release(p); + return ERRNO_DISABLED; +#endif +} + +meshtastic_QueueStatus RadioLibInterface::getQueueStatus() +{ + meshtastic_QueueStatus qs; + + qs.res = qs.mesh_packet_id = 0; + qs.free = txQueue.getFree(); + qs.maxlen = txQueue.getMaxLen(); + + 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; +} + +/** 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 result = (p != NULL); + LOG_DEBUG("cancelSending id=0x%x, removed=%d", id, result); + return result; +} + +/** radio helper thread callback. +We never immediately transmit after any operation (either Rx or Tx). Instead we should wait a random multiple of +'slotTimes' (see definition in RadioInterface.h) taken from a contention window (CW) to lower the chance of collision. +The CW size is determined by setTransmitDelay() and depends either on the current channel utilization or SNR in case +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(); + // LOG_DEBUG("tx complete - starting timer"); + startTransmitTimer(); + break; + case ISR_RX: + handleReceiveInterrupt(); + startReceive(); + // LOG_DEBUG("rx complete - starting timer"); + startTransmitTimer(); + break; + case TRANSMIT_DELAY_COMPLETED: + // 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, try receiving first."); + startReceive(); // try receiving this packet, afterwards we'll be trying to transmit again + setTransmitDelay(); + } 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 = getPacketTime(txp); + airTime->logAirtime(TX_LOG, xmitMsec); + } + } + } else { + // LOG_DEBUG("done with txqueue"); + } + break; + default: + assert(0); // We expected to receive a valid notification from the ISR + } +} + +void RadioLibInterface::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); + startTransmitTimerSNR(p->rx_snr); + } +} + +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(); + // LOG_DEBUG("xmit timer %d", delay); + notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable + } +} + +void RadioLibInterface::startTransmitTimerSNR(float snr) +{ + // If we have work to do and the timer wasn't already scheduled, schedule it now + if (!txQueue.empty()) { + uint32_t delay = getTxDelayMsecWeighted(snr); + // LOG_DEBUG("xmit timer %d", delay); + notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable + } +} + +void RadioLibInterface::handleTransmitInterrupt() +{ + // LOG_DEBUG("handling lora TX interrupt"); + // 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 deffinitely 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; + + 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"); + } +} + +void RadioLibInterface::handleReceiveInterrupt() +{ + uint32_t xmitMsec; + + // 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 receive mode, which shouldn't happen."); + return; + } + + isReceiving = false; + + // read the number of actually received bytes + size_t length = iface->getPacketLength(); + + xmitMsec = getPacketTime(length); + +#ifndef DISABLE_WELCOME_UNSET + if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + LOG_WARN("recv - lora rx disabled because RegionCode_Unset"); + airTime->logAirtime(RX_ALL_LOG, xmitMsec); + return; + } +#endif + + int state = iface->readData((uint8_t *)&radioBuffer, length); + if (state != RADIOLIB_ERR_NONE) { + LOG_ERROR("ignoring received packet due to error=%d", state); + rxBad++; + + airTime->logAirtime(RX_ALL_LOG, xmitMsec); + + } 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("ignoring received packet too short"); + rxBad++; + airTime->logAirtime(RX_ALL_LOG, xmitMsec); + } else { + rxGood++; + // altered packet with "from == 0" can do Remote Node Administration without permission + if (radioBuffer.header.from == 0) { + LOG_WARN("ignoring 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(); + + 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); + + 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, xmitMsec); + + deliverToReceiver(mp); + } + } +} + +void RadioLibInterface::startReceive() +{ + isReceiving = true; + powerMon->setState(meshtastic_PowerMon_State_Lora_RXOn); +} + +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); +} + +/** start an immediate transmit */ +void RadioLibInterface::startSend(meshtastic_MeshPacket *txp) +{ + printPacket("Starting low level send", txp); + if (txp->to == NODENUM_BROADCAST_NO_LORA) { + LOG_DEBUG("Drop Tx packet because dest is broadcast no-lora"); + packetPool.release(txp); + } else if (disabled || !config.lora.tx_enabled) { + LOG_WARN("Drop Tx packet because LoRa Tx disabled"); + packetPool.release(txp); + } 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) + } + + // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register + // bits + enableInterrupt(isrTxLevel0); + } +} \ No newline at end of file diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h new file mode 100644 index 0000000..353176a --- /dev/null +++ b/src/mesh/RadioLibInterface.h @@ -0,0 +1,201 @@ +#pragma once + +#include "MeshPacketQueue.h" +#include "RadioInterface.h" +#include "concurrency/NotifiedWorkerThread.h" + +#include + +// ESP32 has special rules about ISR code +#ifdef ARDUINO_ARCH_ESP32 +#define INTERRUPT_ATTR IRAM_ATTR +#else +#define INTERRUPT_ATTR +#endif + +#define RADIOLIB_PIN_TYPE uint32_t + +/** + * 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, RADIOLIB_PIN_TYPE _busy = RADIOLIB_NC) + : ArduinoHal(spi, spiSettings) + { +#if ARCH_PORTDUINO + busy = _busy; +#endif + }; + + void spiBeginTransaction() override; + void spiEndTransaction() override; +#if ARCH_PORTDUINO + RADIOLIB_PIN_TYPE busy; + void spiTransfer(uint8_t *out, size_t len, uint8_t *in) override; + +#endif +}; + +#if defined(USE_STM32WLx) +/** + * 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(){}; +}; +#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 }; + + /** + * Raw ISR handler that just calls our polymorphic method + */ + static void isrTxLevel0(), isrLevel0Common(PendingISR code); + + MeshPacketQueue txQueue = MeshPacketQueue(MAX_TX_QUEUE); + + protected: + /** + * 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; + + 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; + + /** 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; + + 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 */ + void startTransmitTimer(bool withDelay = true); + + /** timer scaled to SNR of to be flooded packet */ + void startTransmitTimerSNR(float snr); + + 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 + */ + virtual void 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(); + + const char *radioLibErr = "RadioLib err="; +}; \ No newline at end of file diff --git a/src/mesh/RadioLibRF95.cpp b/src/mesh/RadioLibRF95.cpp new file mode 100644 index 0000000..a34c060 --- /dev/null +++ b/src/mesh/RadioLibRF95.cpp @@ -0,0 +1,86 @@ +#if RADIOLIB_EXCLUDE_SX127X != 1 +#include "RadioLibRF95.h" +#include "configuration.h" + +// From datasheet but radiolib doesn't know anything about this +#define SX127X_REG_TCXO 0x4B + +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); + + // 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); + +#ifdef RF95_TCXO + 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); + + state = setBandwidth(bw); + RADIOLIB_ASSERT(state); + + state = setSpreadingFactor(sf); + RADIOLIB_ASSERT(state); + + state = setCodingRate(cr); + RADIOLIB_ASSERT(state); + +#ifdef USE_RF95_RFO + state = setOutputPower(power, true); +#else + state = setOutputPower(power); +#endif + RADIOLIB_ASSERT(state); + + state = setGain(gain); + + return (state); +} + +int16_t RadioLibRF95::setFrequency(float freq) +{ + // RADIOLIB_CHECK_RANGE(freq, 862.0, 1020.0, ERR_INVALID_FREQUENCY); + + // set frequency + return (SX127x::setFrequencyRaw(freq)); +} + +#define RH_RF95_MODEM_STATUS_CLEAR 0x10 +#define RH_RF95_MODEM_STATUS_HEADER_INFO_VALID 0x08 +#define RH_RF95_MODEM_STATUS_RX_ONGOING 0x04 +#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; +} + +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 new file mode 100644 index 0000000..916a332 --- /dev/null +++ b/src/mesh/RadioLibRF95.h @@ -0,0 +1,73 @@ +#pragma once +#if RADIOLIB_EXCLUDE_SX127X != 1 +#include + +/*! + \class RFM95 + + \brief Derived class for %RFM95 modules. Overrides some methods from SX1278 due to different parameter ranges. +*/ +class RadioLibRF95 : public SX1278 +{ + public: + // constructor + + /*! + \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); + + // basic methods + + /*! + \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 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 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 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 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); + + // configuration methods + + /*! + \brief Sets carrier frequency. Allowed values range from 868.0 MHz to 915.0 MHz. + + \param freq Carrier frequency to be set in MHz. + + \returns \ref status_codes + */ + int16_t setFrequency(float freq); + + // Return true if we are actively receiving a message currently + bool isReceiving(); + + /// 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; +}; +#endif \ No newline at end of file diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp new file mode 100644 index 0000000..a2e0936 --- /dev/null +++ b/src/mesh/ReliableRouter.cpp @@ -0,0 +1,269 @@ +#include "ReliableRouter.h" +#include "Default.h" +#include "MeshModule.h" +#include "MeshTypes.h" +#include "configuration.h" +#include "mesh-pb-constants.h" +#include "modules/NodeInfoModule.h" + +// ReliableRouter::ReliableRouter() {} + +/** + * 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); + } + + auto copy = packetPool.allocCopy(*p); + startRetransmission(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); + } + } + + return FloodingRouter::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); + + // 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("generating 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); + + 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); + } + + /* Resend implicit ACKs for repeated packets (hopStart equals hopLimit); + * this way if an implicit ACK is dropped and a packet is resent we'll rebroadcast again. + * Resending real ACKs is omitted, as you might receive a packet multiple times due to flooding and + * flooding this ACK back to the original sender already adds redundancy. */ + bool isRepeated = p->hop_start == 0 ? (p->hop_limit == HOP_RELIABLE) : (p->hop_start == p->hop_limit); + if (wasSeenRecently(p, false) && isRepeated && !MeshModule::currentReply && !isToUs(p)) { + LOG_DEBUG("Resending implicit ack for a repeated floodmsg"); + meshtastic_MeshPacket *tosend = packetPool.allocCopy(*p); + tosend->hop_limit--; // bump down the hop count + Router::send(tosend); + } + + return FloodingRouter::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 an ack packet (do check wasSeenRecently), clear out any retransmissions and + * forward the ack to the application layer. + * + * If we receive a nak packet (do check wasSeenRecently), clear out any retransmissions + * and forward the nak to the application layer. + * + * 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 (p->want_ack) { + if (MeshModule::currentReply) { + LOG_DEBUG("Another module replied to this message, no need for 2nd ack"); + } else if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, p->hop_start, p->hop_limit); + } 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(), + p->hop_start, p->hop_limit); + } 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(), p->hop_start, + p->hop_limit); + } + } + 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) { + 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 + FloodingRouter::sniffReceived(p, c); +} + +#define NUM_RETRANSMISSIONS 3 + +PendingPacket::PendingPacket(meshtastic_MeshPacket *p) +{ + packet = p; + numRetransmissions = NUM_RETRANSMISSIONS - 1; // We subtract one, because we assume the user just did the first send +} + +PendingPacket *ReliableRouter::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 ReliableRouter::stopRetransmission(NodeNum from, PacketId id) +{ + auto key = GlobalPacketId(from, id); + return stopRetransmission(key); +} + +bool ReliableRouter::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_RETRANSMISSIONS - 1) { + // remove the 'original' (identified by originator and packet->id) from the txqueue and free it + cancelSending(getFrom(p), p->id); + // now free the pooled copy for retransmission too + packetPool.release(p); + } + auto numErased = pending.erase(key); + assert(numErased == 1); + return true; + } else + return false; +} + +/** + * Add p to the list of packets to retransmit occasionally. We will free it once we stop retransmitting. + */ +PendingPacket *ReliableRouter::startRetransmission(meshtastic_MeshPacket *p) +{ + auto id = GlobalPacketId(p); + auto rec = PendingPacket(p); + + stopRetransmission(getFrom(p), p->id); + + setNextTx(&rec); + pending[id] = rec; + + return &pending[id]; +} + +/** + * Do any retransmissions that are scheduled (FIXME - for the time being called from loop) + */ +int32_t ReliableRouter::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; + + bool stillValid = true; // assume we'll keep this record around + + // FIXME, handle 51 day rolloever here!!! + if (p.nextTxMsec <= now) { + if (p.numRetransmissions == 0) { + 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 reliable retransmission fr=0x%x,to=0x%x,id=0x%x, tries left=%d", p.packet->from, p.packet->to, + p.packet->id, p.numRetransmissions); + + // 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); + } + } + + return d; +} + +void ReliableRouter::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 +} \ No newline at end of file diff --git a/src/mesh/ReliableRouter.h b/src/mesh/ReliableRouter.h new file mode 100644 index 0000000..259da72 --- /dev/null +++ b/src/mesh/ReliableRouter.h @@ -0,0 +1,123 @@ +#pragma once + +#include "FloodingRouter.h" +#include + +/** + * An identifier for a globalally unique message - a pair of the sending nodenum and the packet id assigned + * to that message + */ +struct GlobalPacketId { + NodeNum node; + PacketId 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; + } + + GlobalPacketId(NodeNum _from, PacketId _id) + { + node = _from; + id = _id; + } +}; + +/** + * A packet queued for retransmission + */ +struct PendingPacket { + meshtastic_MeshPacket *packet; + + /** The next time we should try to retransmit this packet */ + uint32_t nextTxMsec = 0; + + /** Starts at NUM_RETRANSMISSIONS -1(normally 3) and counts down. Once zero it will be removed from the list */ + uint8_t numRetransmissions = 0; + + PendingPacket() {} + explicit PendingPacket(meshtastic_MeshPacket *p); +}; + +class GlobalPacketIdHashFunction +{ + public: + size_t operator()(const GlobalPacketId &p) const { return (std::hash()(p.node)) ^ (std::hash()(p.id)); } +}; + +/** + * This is a mixin that extends Router with the ability to do (one hop only) reliable message sends. + */ +class ReliableRouter : public FloodingRouter +{ + private: + std::unordered_map pending; + + 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; + + /** 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 + auto d = doRetransmissions(); + + int32_t r = FloodingRouter::runOnce(); + + return min(d, r); + } + + protected: + /** + * Look for acks/naks or someone retransmitting us + */ + 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); + + /** + * We hook this method so we can see packets before FloodingRouter says they should be discarded + */ + virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) override; + + /** + * Add p to the list of packets to retransmit occasionally. We will free it once we stop retransmitting. + */ + PendingPacket *startRetransmission(meshtastic_MeshPacket *p); + + private: + /** + * 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(); + + void setNextTx(PendingPacket *pending); +}; diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp new file mode 100644 index 0000000..84ae7bb --- /dev/null +++ b/src/mesh/Router.cpp @@ -0,0 +1,671 @@ +#include "Router.h" +#include "Channels.h" +#include "CryptoEngine.h" +#include "MeshRadio.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "RTC.h" +#include "configuration.h" +#include "main.h" +#include "mesh-pb-constants.h" +#include "meshUtils.h" +#include "modules/RoutingModule.h" +#if !MESHTASTIC_EXCLUDE_MQTT +#include "mqtt/MQTT.h" +#endif +#include "Default.h" +#if ARCH_PORTDUINO +#include "platform/portduino/PortduinoGlue.h" +#endif +#if ENABLE_JSON_LOGGING || ARCH_PORTDUINO +#include "serialization/MeshPacketSerializer.h" +#endif +#include "../userPrefs.h" + +#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 +#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 MemoryPool staticPool(MAX_PACKETS); +static MemoryDynamic staticPool; + +Allocator &packetPool = staticPool; + +static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1] __attribute__((__aligned__)); +static uint8_t ScratchEncrypted[MAX_LORA_PAYLOAD_LEN + 1] __attribute__((__aligned__)); + +/** + * Constructor + * + * 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 + + /* 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); + + // init Lockguard for crypt operations + assert(!cryptLock); + cryptLock = new concurrency::Lock(); +} + +/** + * 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); + } + + // LOG_DEBUG("sleeping 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 + */ +void Router::enqueueReceivedMessage(meshtastic_MeshPacket *p) +{ + if (fromRadioQueue.enqueue(p, 0)) { // NOWAIT - fixme, if queue is full, delete older messages + + // Nasty hack because our threading is primitive. interfaces shouldn't need to know about routers FIXME + setReceivedMessage(); + } else { + printPacket("BUG! fromRadioQueue is full! Discarding!", p); + packetPool.release(p); + } +} + +/// 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; + + 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); + } + + 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; +} + +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 + + 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 hopStart, + uint8_t hopLimit) +{ + routingModule->sendAckNak(err, to, idFrom, chIndex, hopStart, hopLimit); +} + +void Router::abortSendAndNak(meshtastic_Routing_Error err, meshtastic_MeshPacket *p) +{ + LOG_ERROR("Error=%d, returning NAK and dropping 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; +} + +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); + } + + if (!p->channel && !p->pki_encrypted) { // don't override if a channel was requested + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->to); + if (node && node->user.public_key.size == 0) { + p->channel = node->channel; + LOG_DEBUG("localSend to channel %d", p->channel); + } + } + + return send(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. + */ +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) { +#ifdef DEBUG_PORT + uint8_t silentMinutes = airTime->getSilentMinutes(hourlyTxPercent, myRegion->dutyCycle); + LOG_WARN("Duty cycle limit exceeded. Aborting send for now, you can send again in %d minutes.", 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 minutes.", silentMinutes); + service->sendClientNotification(cn); +#endif + 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 + + // 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); + + // 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 (!(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 + meshtastic_MeshPacket *p_decoded = packetPool.allocCopy(*p); + + auto encodeResult = perhapsEncode(p); + if (encodeResult != meshtastic_Routing_Error_NONE) { + packetPool.release(p_decoded); + 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); + } + + 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) +{ + return iface ? iface->cancelSending(from, id) : false; +} + +/** + * 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 +} + +bool perhapsDecode(meshtastic_MeshPacket *p) +{ + concurrency::LockGuard g(cryptLock); + + if (config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER && + config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_ALL_SKIP_DECODING) + return false; + + 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 false; + } + + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) + return true; // 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 false; + } + bool decrypted = false; + ChannelIndex chIndex = 0; + memcpy(bytes, p->encrypted.bytes, + rawSize); // we have to copy into a scratch buffer, because these bytes are a union with the decoded protobuf + memcpy(ScratchEncrypted, p->encrypted.bytes, rawSize); +#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("Attempting PKI decryption"); + + if (crypto->decryptCurve25519(p->from, nodeDB->getMeshNode(p->from)->user.public_key, p->id, rawSize, ScratchEncrypted, + bytes)) { + LOG_INFO("PKI Decryption worked!"); + memset(&p->decoded, 0, sizeof(p->decoded)); + rawSize -= MESHTASTIC_PKC_OVERHEAD; + if (pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &p->decoded) && + p->decoded.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; + // memcpy(bytes, ScratchEncrypted, rawSize); // TODO: Rename the bytes buffers + // chIndex = 8; + } else { + LOG_ERROR("PKC Decrypted, but pb_decode failed!"); + return false; + } + } 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)) { + // 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 + memset(&p->decoded, 0, sizeof(p->decoded)); + if (!pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &p->decoded)) { + LOG_ERROR("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)!", p->id); + } else if (p->decoded.portnum == meshtastic_PortNum_UNKNOWN_APP) { + LOG_ERROR("Invalid portnum (bad psk?)!"); + } else { + decrypted = true; + break; + } + } + } + } + if (decrypted) { + // parsing was successful + p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded + 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 (settingsStrings[traceFilename] != "" || settingsMap[logoutputlevel] == level_trace) { + LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str()); + } +#endif + return true; + } else { + LOG_WARN("No suitable channel found for decoding, hash was 0x%x!", p->channel); + return false; + } +} + +/** Return 0 for success or a Routing_Errror code for failure + */ +meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) +{ + concurrency::LockGuard g(cryptLock); + + 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("Using 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 + if ( + // Don't use PKC with Ham mode + !owner.is_licensed && + // Don't use PKC if it's not explicitly requested and a non-primary channel is requested + !(p->pki_encrypted != true && p->channel > 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("Using 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, ScratchEncrypted); + numbytes += MESHTASTIC_PKC_OVERHEAD; + memcpy(p->encrypted.bytes, ScratchEncrypted, numbytes); + 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 sending + 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 sending + 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(); +} + +/** + * 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 + // Store a copy of encrypted packet for MQTT + meshtastic_MeshPacket *p_encrypted = packetPool.allocCopy(*p); + + // Take those raw bytes and convert them back into a well structured protobuf we can understand + bool decoded = perhapsDecode(p); + if (decoded) { + // 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, ignoring neighbor packet"); + cancelSending(p->from, p->id); + skipHandle = true; + } + +#if USERPREFS_EVENT_MODE + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && + (p->decoded.portnum == meshtastic_PortNum_ATAK_FORWARDER || p->decoded.portnum == meshtastic_PortNum_ATAK_PLUGIN || + p->decoded.portnum == meshtastic_PortNum_PAXCOUNTER_APP || p->decoded.portnum == meshtastic_PortNum_IP_TUNNEL_APP || + p->decoded.portnum == meshtastic_PortNum_AUDIO_APP || p->decoded.portnum == meshtastic_PortNum_PRIVATE_APP || + p->decoded.portnum == meshtastic_PortNum_DETECTION_SENSOR_APP || + p->decoded.portnum == meshtastic_PortNum_RANGE_TEST_APP || + p->decoded.portnum == meshtastic_PortNum_REMOTE_HARDWARE_APP)) { + LOG_DEBUG("Ignoring packet on blacklisted portnum during event"); + cancelSending(p->from, p->id); + skipHandle = true; + } +#endif + } else { + printPacket("packet decoding failed or skipped (no PSK?)", p); + } + + // call modules here + if (!skipHandle) { + MeshModule::callModules(*p, src); + +#if !MESHTASTIC_EXCLUDE_MQTT + // 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 (!decoded && 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 ((decoded || 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 +} + +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 (settingsStrings[traceFilename] != "" || settingsMap[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("Ignoring msg, 0x%x is in our ignore list", p->from); + packetPool.release(p); + return; + } + + if (p->from == NODENUM_BROADCAST) { + LOG_DEBUG("Ignoring 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); +} \ No newline at end of file diff --git a/src/mesh/Router.h b/src/mesh/Router.h new file mode 100644 index 0000000..8ebc1a3 --- /dev/null +++ b/src/mesh/Router.h @@ -0,0 +1,159 @@ +#pragma once + +#include "Channels.h" +#include "MemoryPool.h" +#include "MeshTypes.h" +#include "Observer.h" +#include "PointerQueue.h" +#include "RadioInterface.h" +#include "concurrency/OSThread.h" + +/** + * A mesh aware router that supports multiple interfaces. + */ +class Router : protected concurrency::OSThread +{ + 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; + + public: + /** + * Constructor + * + */ + Router(); + + /** + * 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; + + /** + * 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); + + /** 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 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(); + + /** + * RadioInterface calls this to queue up packets that have been received from the radio. The router is now responsible for + * freeing the packet + */ + 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); + + /* 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; + + 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; } + + /** + * 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 hopStart = 0, + uint8_t hopLimit = 0); + + 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); + + /** Frees the provided packet, and generates a NAK indicating the speicifed error while sending */ + void abortSendAndNak(meshtastic_Routing_Error err, meshtastic_MeshPacket *p); +}; + +/** FIXME - move this into a mesh packet class + * Remove any encryption and decode the protobufs inside this packet (if necessary). + * + * @return true for success, false for corrupt packet. + */ +bool perhapsDecode(meshtastic_MeshPacket *p); + +/** Return 0 for success or a Routing_Errror code for failure + */ +meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p); + +extern Router *router; + +/// Generate a unique packet id +// FIXME, move this someplace better +PacketId generatePacketId(); + +#define BITFIELD_WANT_RESPONSE_SHIFT 1 +#define BITFIELD_OK_TO_MQTT_SHIFT 0 +#define BITFIELD_WANT_RESPONSE_MASK (1 << BITFIELD_WANT_RESPONSE_SHIFT) +#define BITFIELD_OK_TO_MQTT_MASK (1 << BITFIELD_OK_TO_MQTT_SHIFT) \ No newline at end of file diff --git a/src/mesh/STM32WLE5JCInterface.cpp b/src/mesh/STM32WLE5JCInterface.cpp new file mode 100644 index 0000000..499db91 --- /dev/null +++ b/src/mesh/STM32WLE5JCInterface.cpp @@ -0,0 +1,42 @@ +#include "STM32WLE5JCInterface.h" +#include "configuration.h" +#include "error.h" + +#ifndef STM32WLx_MAX_POWER +#define STM32WLx_MAX_POWER 22 +#endif + +#ifdef ARCH_STM32WL + +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(); + + lora.setRfSwitchTable(rfswitch_pins, rfswitch_table); + + if (power > STM32WLx_MAX_POWER) // This chip has lower power limits than some + power = STM32WLx_MAX_POWER; + + limitPower(); + + int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); + + 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); + + if (res == RADIOLIB_ERR_NONE) + startReceive(); // start receiving + + return res == RADIOLIB_ERR_NONE; +} + +#endif // ARCH_STM32WL diff --git a/src/mesh/STM32WLE5JCInterface.h b/src/mesh/STM32WLE5JCInterface.h new file mode 100644 index 0000000..fad7933 --- /dev/null +++ b/src/mesh/STM32WLE5JCInterface.h @@ -0,0 +1,31 @@ +#pragma once + +#include "SX126xInterface.h" + +#ifdef ARCH_STM32WL + +/** + * 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); + + virtual bool init() override; +}; + +// https://github.com/Seeed-Studio/LoRaWan-E5-Node/blob/main/Middlewares/Third_Party/SubGHz_Phy/stm32_radio_driver/radio_driver.c +static const float tcxoVoltage = 1.7; + +/* https://wiki.seeedstudio.com/LoRa-E5_STM32WLE5JC_Module/ + * Wio-E5 module ONLY transmits through RFO_HP + * Receive: PA4=1, PA5=0 + * Transmit(high output power, SMPS mode): PA4=0, PA5=1 */ +static const RADIOLIB_PIN_TYPE rfswitch_pins[5] = {PA4, PA5, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; + +static const Module::RfSwitchMode_t rfswitch_table[4] = { + {STM32WLx::MODE_IDLE, {LOW, LOW}}, {STM32WLx::MODE_RX, {HIGH, LOW}}, {STM32WLx::MODE_TX_HP, {LOW, HIGH}}, END_OF_MODE_TABLE}; + +#endif // ARCH_STM32WL \ No newline at end of file diff --git a/src/mesh/SX1262Interface.cpp b/src/mesh/SX1262Interface.cpp new file mode 100644 index 0000000..4c0dea0 --- /dev/null +++ b/src/mesh/SX1262Interface.cpp @@ -0,0 +1,11 @@ +#if RADIOLIB_EXCLUDE_SX126X != 1 +#include "SX1262Interface.h" +#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) +{ +} +#endif \ No newline at end of file diff --git a/src/mesh/SX1262Interface.h b/src/mesh/SX1262Interface.h new file mode 100644 index 0000000..6e4616c --- /dev/null +++ b/src/mesh/SX1262Interface.h @@ -0,0 +1,15 @@ +#if RADIOLIB_EXCLUDE_SX126X != 1 +#pragma once + +#include "SX126xInterface.h" + +/** + * 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); +}; +#endif \ No newline at end of file diff --git a/src/mesh/SX1268Interface.cpp b/src/mesh/SX1268Interface.cpp new file mode 100644 index 0000000..fe6e9af --- /dev/null +++ b/src/mesh/SX1268Interface.cpp @@ -0,0 +1,20 @@ +#if RADIOLIB_EXCLUDE_SX126X != 1 +#include "SX1268Interface.h" +#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) +{ +} + +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 new file mode 100644 index 0000000..cc6dd35 --- /dev/null +++ b/src/mesh/SX1268Interface.h @@ -0,0 +1,17 @@ +#pragma once +#if RADIOLIB_EXCLUDE_SX126X != 1 + +#include "SX126xInterface.h" + +/** + * Our adapter for SX1268 radios + */ +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); +}; +#endif \ No newline at end of file diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp new file mode 100644 index 0000000..c88ed39 --- /dev/null +++ b/src/mesh/SX126xInterface.cpp @@ -0,0 +1,346 @@ +#if RADIOLIB_EXCLUDE_SX126X != 1 +#include "SX126xInterface.h" +#include "configuration.h" +#include "error.h" +#include "mesh/NodeDB.h" +#ifdef ARCH_PORTDUINO +#include "PortduinoGlue.h" +#endif + +#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) +#ifndef SX126X_MAX_POWER +#define SX126X_MAX_POWER 22 +#endif + +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); +} + +/// 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() +{ + +// 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); +#endif + +#if ARCH_PORTDUINO + float tcxoVoltage = 0; + if (settingsMap[dio3_tcxo_voltage]) + tcxoVoltage = 1.8; + if (settingsMap[sx126x_ant_sw] != RADIOLIB_NC) { + digitalWrite(settingsMap[sx126x_ant_sw], HIGH); + pinMode(settingsMap[sx126x_ant_sw], OUTPUT); + } +// FIXME: correct logic to default to not using TCXO if no voltage is specified for SX126X_DIO3_TCXO_VOLTAGE +#elif !defined(SX126X_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/SX126x/SX126x.h#L471C26-L471C104 + // (DIO3 is free to be used as an IRQ) +#elif !defined(TCXO_OPTIONAL) + float tcxoVoltage = SX126X_DIO3_TCXO_VOLTAGE; + // (DIO3 is not free to be used as an IRQ) +#endif + if (tcxoVoltage == 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); + + // 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(); + + if (power > SX126X_MAX_POWER) // Clamp power to maximum defined level + power = SX126X_MAX_POWER; + + limitPower(); + + 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) + 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 neccessary, 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 (settingsMap[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("Using MCU pin %i as RXEN and pin %i as TXEN to control RF switching", settingsMap[rxen], settingsMap[txen]); + lora.setRfSwitchPins(settingsMap[rxen], settingsMap[txen]); + } +#else +#ifndef SX126X_RXEN +#define SX126X_RXEN 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"); +#endif + if (res == RADIOLIB_ERR_NONE) { + LOG_DEBUG("Using 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 0 + // Read/write a register we are not using (only used for FSK mode) to test SPI comms + uint8_t crcLSB = 0; + int err = lora.readRegister(SX126X_REG_CRC_POLYNOMIAL_LSB, &crcLSB, 1); + if(err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure); + + //if(crcLSB != 0x0f) + // RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure); + + crcLSB = 0x5a; + err = lora.writeRegister(SX126X_REG_CRC_POLYNOMIAL_LSB, &crcLSB, 1); + if(err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure); + + err = lora.readRegister(SX126X_REG_CRC_POLYNOMIAL_LSB, &crcLSB, 1); + if(err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure); + + if(crcLSB != 0x5a) + RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure); + // 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) + startReceive(); // start receiving + + return res == RADIOLIB_ERR_NONE; +} + +template bool SX126xInterface::reconfigure() +{ + RadioLibInterface::reconfigure(); + + // 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); + + 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.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.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); + + 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); + + startReceive(); // restart receiving + + return RADIOLIB_ERR_NONE; +} + +template void INTERRUPT_ATTR SX126xInterface::disableInterrupt() +{ + lora.clearDio1Action(); +} + +template void SX126xInterface::setStandby() +{ + checkNotification(); // handle any pending interrupts before we force standby + + int err = lora.standby(); + + 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(); +} + +/** + * 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()); +} + +/** We override to turn on transmitter power as needed. + */ +template void SX126xInterface::configHardwareForSend() +{ + RadioLibInterface::configHardwareForSend(); +} + +// For power draw measurements, helpful to force radio to stay sleeping +// #define SLEEP_ONLY + +template void SX126xInterface::startReceive() +{ +#ifdef SLEEP_ONLY + sleep(); +#else + + setStandby(); + + // We use a 16 bit preamble so this should save some power by letting radio sit in standby mostly. + // Furthermore, we need the PREAMBLE_DETECTED and HEADER_VALID IRQ flag to detect whether we are actively receiving + int err = lora.startReceiveDutyCycleAuto(preambleLength, 8, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | RADIOLIB_IRQ_PREAMBLE_DETECTED); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("SX126X startReceiveDutyCycleAuto %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); + + RadioLibInterface::startReceive(); + + // 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 + int16_t result; + + setStandby(); + result = lora.scanChannel(); + 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; +} + +/** 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::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); + + // 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); +#endif + + return true; +} +#endif \ No newline at end of file diff --git a/src/mesh/SX126xInterface.h b/src/mesh/SX126xInterface.h new file mode 100644 index 0000000..45b39a6 --- /dev/null +++ b/src/mesh/SX126xInterface.h @@ -0,0 +1,72 @@ +#pragma once +#if RADIOLIB_EXCLUDE_SX126X != 1 + +#include "RadioLibInterface.h" + +/** + * \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); + + /// 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; + + /// 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; } + + protected: + float currentLimit = 140; // Higher OCP limit for SX126x PA + + /** + * Specific module instance + */ + T lora; + + /** + * 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); } + + /** 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; + + /** + * Start waiting to receive a message + */ + virtual void startReceive() 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; + + virtual void setStandby() override; +}; +#endif \ No newline at end of file diff --git a/src/mesh/SX1280Interface.cpp b/src/mesh/SX1280Interface.cpp new file mode 100644 index 0000000..9e0d421 --- /dev/null +++ b/src/mesh/SX1280Interface.cpp @@ -0,0 +1,11 @@ +#if RADIOLIB_EXCLUDE_SX128X != 1 +#include "SX1280Interface.h" +#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) +{ +} +#endif \ No newline at end of file diff --git a/src/mesh/SX1280Interface.h b/src/mesh/SX1280Interface.h new file mode 100644 index 0000000..534dd80 --- /dev/null +++ b/src/mesh/SX1280Interface.h @@ -0,0 +1,15 @@ +#pragma once +#if RADIOLIB_EXCLUDE_SX128X != 1 +#include "SX128xInterface.h" + +/** + * 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); +}; +#endif diff --git a/src/mesh/SX128xInterface.cpp b/src/mesh/SX128xInterface.cpp new file mode 100644 index 0000000..9fe86cc --- /dev/null +++ b/src/mesh/SX128xInterface.cpp @@ -0,0 +1,318 @@ +#if RADIOLIB_EXCLUDE_SX128X != 1 +#include "SX128xInterface.h" +#include "Throttle.h" +#include "configuration.h" +#include "error.h" +#include "mesh/NodeDB.h" + +#if ARCH_PORTDUINO +#include "PortduinoGlue.h" +#endif + +// Particular boards might define a different max power based on what their hardware can do +#ifndef SX128X_MAX_POWER +#define SX128X_MAX_POWER 13 +#endif + +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); +} + +/// 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() +{ +#ifdef SX128X_POWER_EN + 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); +#endif + +#if ARCH_PORTDUINO + if (settingsMap[rxen] != RADIOLIB_NC) { + pinMode(settingsMap[rxen], OUTPUT); + digitalWrite(settingsMap[rxen], LOW); // Set low before becoming an output + } + if (settingsMap[txen] != RADIOLIB_NC) { + pinMode(settingsMap[txen], OUTPUT); + digitalWrite(settingsMap[txen], 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 +#endif +#if defined(SX128X_TXEN) && (SX128X_TXEN != RADIOLIB_NC) + pinMode(SX128X_TXEN, OUTPUT); + digitalWrite(SX128X_TXEN, LOW); +#endif +#endif + + RadioLibInterface::init(); + + if (power > SX128X_MAX_POWER) // This chip has lower power limits than some + power = SX128X_MAX_POWER; + + limitPower(); + + 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); + + if ((config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && (res == RADIOLIB_ERR_INVALID_FREQUENCY)) { + LOG_WARN("Radio chip 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(); +#elif defined(ARCH_NRF52) + NVIC_SystemReset(); +#else + LOG_ERROR("FIXME implement reboot for this platform. Skipping 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); + +#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); + } +#elif ARCH_PORTDUINO + if (res == RADIOLIB_ERR_NONE && settingsMap[rxen] != RADIOLIB_NC && settingsMap[txen] != RADIOLIB_NC) { + lora.setRfSwitchPins(settingsMap[rxen], settingsMap[txen]); + } +#endif + + if (res == RADIOLIB_ERR_NONE) + res = lora.setCRC(2); + + if (res == RADIOLIB_ERR_NONE) + startReceive(); // start receiving + + return res == RADIOLIB_ERR_NONE; +} + +template bool SX128xInterface::reconfigure() +{ + RadioLibInterface::reconfigure(); + + // 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); + + 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.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.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; + + 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 + + return RADIOLIB_ERR_NONE; +} + +template void INTERRUPT_ATTR SX128xInterface::disableInterrupt() +{ + lora.clearDio1Action(); +} + +template bool SX128xInterface::wideLora() +{ + return true; +} + +template void SX128xInterface::setStandby() +{ + checkNotification(); // handle any pending interrupts before we force standby + + int err = lora.standby(); + + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("SX128x standby %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); +#if ARCH_PORTDUINO + if (settingsMap[rxen] != RADIOLIB_NC) { + digitalWrite(settingsMap[rxen], LOW); + } + if (settingsMap[txen] != RADIOLIB_NC) { + digitalWrite(settingsMap[txen], 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); +#endif +#if defined(SX128X_TXEN) && (SX128X_TXEN != RADIOLIB_NC) + 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(); +} + +/** + * 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()); +} + +/** We override to turn on transmitter power as needed. + */ +template void SX128xInterface::configHardwareForSend() +{ +#if ARCH_PORTDUINO + if (settingsMap[txen] != RADIOLIB_NC) { + digitalWrite(settingsMap[txen], HIGH); + } + if (settingsMap[rxen] != RADIOLIB_NC) { + digitalWrite(settingsMap[rxen], 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); +#endif +#if defined(SX128X_RXEN) && (SX128X_RXEN != RADIOLIB_NC) + digitalWrite(SX128X_RXEN, LOW); +#endif +#endif + + RadioLibInterface::configHardwareForSend(); +} + +// For power draw measurements, helpful to force radio to stay sleeping +// #define SLEEP_ONLY + +template void SX128xInterface::startReceive() +{ +#ifdef SLEEP_ONLY + sleep(); +#else + + setStandby(); + +#if ARCH_PORTDUINO + if (settingsMap[rxen] != RADIOLIB_NC) { + digitalWrite(settingsMap[rxen], HIGH); + } + if (settingsMap[txen] != RADIOLIB_NC) { + digitalWrite(settingsMap[txen], 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); +#endif +#if defined(SX128X_TXEN) && (SX128X_TXEN != RADIOLIB_NC) + digitalWrite(SX128X_TXEN, LOW); +#endif +#endif + + // We use the PREAMBLE_DETECTED and HEADER_VALID IRQ flag to detect whether we are actively receiving + int err = lora.startReceive(RADIOLIB_SX128X_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | RADIOLIB_IRQ_PREAMBLE_DETECTED); + + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("SX128X startReceive %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); + + RadioLibInterface::startReceive(); + + // 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 + int16_t result; + + setStandby(); + result = lora.scanChannel(); + 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; +} + +/** 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::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); + + // 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); +#endif + + return true; +} +#endif \ No newline at end of file diff --git a/src/mesh/SX128xInterface.h b/src/mesh/SX128xInterface.h new file mode 100644 index 0000000..bba31da --- /dev/null +++ b/src/mesh/SX128xInterface.h @@ -0,0 +1,70 @@ +#pragma once + +#include "RadioLibInterface.h" + +/** + * \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); + + /// 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; + + /// 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; + + bool isIRQPending() override { return lora.getIrqFlags() != 0; } + + protected: + /** + * Specific module instance + */ + T lora; + + /** + * 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); } + + /** 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; + + /** + * Start waiting to receive a message + */ + virtual void startReceive() 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; + + virtual void setStandby() override; +}; diff --git a/src/mesh/SinglePortModule.h b/src/mesh/SinglePortModule.h new file mode 100644 index 0000000..e43de09 --- /dev/null +++ b/src/mesh/SinglePortModule.h @@ -0,0 +1,39 @@ +#pragma once +#include "MeshModule.h" +#include "Router.h" + +/** + * 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; + + 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; } + + /** + * 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; + } +}; \ No newline at end of file diff --git a/src/mesh/StreamAPI.cpp b/src/mesh/StreamAPI.cpp new file mode 100644 index 0000000..4a42e51 --- /dev/null +++ b/src/mesh/StreamAPI.cpp @@ -0,0 +1,153 @@ +#include "StreamAPI.h" +#include "PowerFSM.h" +#include "RTC.h" +#include "Throttle.h" +#include "configuration.h" + +#define START1 0x94 +#define START2 0xc3 +#define HEADER_LEN 4 + +int32_t StreamAPI::runOncePart() +{ + auto result = readStream(); + writeStream(); + checkConnectionTimeout(); + return result; +} + +/** + * 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 + + 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; + + 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); + + 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); + + 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); + } + } + } + + // 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); + } +} + +/** + * 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; + + 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; + + // 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; + + 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)); +} + +/// Hookable to find out when connection changes +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); + } +} \ No newline at end of file diff --git a/src/mesh/StreamAPI.h b/src/mesh/StreamAPI.h new file mode 100644 index 0000000..45cbb23 --- /dev/null +++ b/src/mesh/StreamAPI.h @@ -0,0 +1,88 @@ +#pragma once + +#include "PhoneAPI.h" +#include "Stream.h" +#include "concurrency/OSThread.h" + +// A To/FromRadio packet + our 32 bit header +#define MAX_STREAM_BUF_SIZE (MAX_TO_FROM_RADIO_SIZE + sizeof(uint32_t)) + +/** + * A version of our 'phone' API that talks over a Stream. So therefore well suited to use with serial links + * or TCP connections. + * + * ## 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). + +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. + +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; + + 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; + + 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(); + + private: + /** + * Read any rx chars from the link and call handleToRadio + */ + int32_t readStream(); + + /** + * call getFromRadio() and deliver encapsulated packets to the Stream + */ + void writeStream(); + + protected: + /** + * Send a FromRadio.rebooted = true packet to the phone + */ + void emitRebooted(); + + 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; + + /** + * 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; + + /// 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); +}; \ No newline at end of file diff --git a/src/mesh/Throttle.cpp b/src/mesh/Throttle.cpp new file mode 100644 index 0000000..f278cc8 --- /dev/null +++ b/src/mesh/Throttle.cpp @@ -0,0 +1,35 @@ +#include "Throttle.h" +#include + +/// @brief Execute a function throttled to a minimum interval +/// @param lastExecutionMs Pointer to the last execution time in milliseconds +/// @param minumumIntervalMs Minimum execution interval in milliseconds +/// @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(); + + 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 diff --git a/src/mesh/Throttle.h b/src/mesh/Throttle.h new file mode 100644 index 0000000..8b4bb5d --- /dev/null +++ b/src/mesh/Throttle.h @@ -0,0 +1,10 @@ +#pragma once +#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); +}; \ No newline at end of file diff --git a/src/mesh/TypeConversions.cpp b/src/mesh/TypeConversions.cpp new file mode 100644 index 0000000..550f870 --- /dev/null +++ b/src/mesh/TypeConversions.cpp @@ -0,0 +1,106 @@ +#include "TypeConversions.h" +#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; + + 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; + + 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; +} + +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; +} + +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; +} + +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; + return lite; +} + +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; + + return user; +} \ No newline at end of file diff --git a/src/mesh/TypeConversions.h b/src/mesh/TypeConversions.h new file mode 100644 index 0000000..19e471f --- /dev/null +++ b/src/mesh/TypeConversions.h @@ -0,0 +1,15 @@ +#include "mesh/generated/meshtastic/deviceonly.pb.h" +#include "mesh/generated/meshtastic/mesh.pb.h" + +#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); +}; diff --git a/src/mesh/TypedQueue.h b/src/mesh/TypedQueue.h new file mode 100644 index 0000000..f7d016f --- /dev/null +++ b/src/mesh/TypedQueue.h @@ -0,0 +1,115 @@ +#pragma once + +#include +#include + +#include "concurrency/OSThread.h" +#include "freertosinc.h" + +#ifdef HAS_FREE_RTOS + +/** + * 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; + + public: + explicit TypedQueue(int maxElements) : h(xQueueCreate(maxElements, sizeof(T))) { assert(h); } + + ~TypedQueue() { vQueueDelete(h); } + + int numFree() { return uxQueueSpacesAvailable(h); } + + bool isEmpty() { return uxQueueMessagesWaiting(h) == 0; } + + 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; + } + + 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 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; } +}; + +#else + +#include + +/** + * 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; + + public: + explicit TypedQueue(int maxElements) {} + + int numFree() { return 1; } // Always claim 1 free, because we can grow to any size + + bool isEmpty() { return q.empty(); } + + int numUsed() { return q.size(); } + + bool enqueue(T x, TickType_t maxWait = portMAX_DELAY) + { + if (reader) { + reader->setInterval(0); + concurrency::mainDelay.interrupt(); + } + + q.push(x); + return true; + } + + // bool enqueueFromISR(T x, BaseType_t *higherPriWoken) { return xQueueSendToBackFromISR(h, &x, higherPriWoken) == pdTRUE; } + + 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 diff --git a/src/mesh/aes-ccm.cpp b/src/mesh/aes-ccm.cpp new file mode 100644 index 0000000..b9af14f --- /dev/null +++ b/src/mesh/aes-ccm.cpp @@ -0,0 +1,157 @@ +/* + * Counter with CBC-MAC (CCM) with AES + * + * Copyright (c) 2010-2012, Jouni Malinen + * + * This software may be distributed under the terms of the BSD license. + * See README for more details. + */ +#define AES_BLOCK_SIZE 16 +#include "aes-ccm.h" +#if !MESHTASTIC_EXCLUDE_PKI + +static inline 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) +{ + uint32_t *d = (uint32_t *)dst; + uint32_t *s = (uint32_t *)src; + *d++ ^= *s++; + *d++ ^= *s++; + *d++ ^= *s++; + *d++ ^= *s++; +} +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 (memcmp(x, t, M) != 0) { // FIXME make const comp + 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 new file mode 100644 index 0000000..6b8edcd --- /dev/null +++ b/src/mesh/aes-ccm.h @@ -0,0 +1,10 @@ +#pragma once +#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); + +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/ServerAPI.cpp b/src/mesh/api/ServerAPI.cpp new file mode 100644 index 0000000..7705858 --- /dev/null +++ b/src/mesh/api/ServerAPI.cpp @@ -0,0 +1,81 @@ +#include "ServerAPI.h" +#include "configuration.h" +#include + +template +ServerAPI::ServerAPI(T &_client) : StreamAPI(&client), concurrency::OSThread("ServerAPI"), client(_client) +{ + LOG_INFO("Incoming API connection"); +} + +template ServerAPI::~ServerAPI() +{ + client.stop(); +} + +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 int32_t ServerAPI::runOnce() +{ + if (client.connected()) { + return StreamAPI::runOncePart(); + } else { + LOG_INFO("Client dropped connection, suspending 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 int32_t APIServerPort::runOnce() +{ +#ifdef ARCH_ESP32 +#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) + auto client = U::accept(); +#else + auto client = U::available(); +#endif +#else + auto client = U::available(); +#endif + 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, trying again in %dms", waitTime); + return waitTime; + } +#endif + LOG_INFO("Force closing previous TCP connection"); + delete openAPI; + } + + openAPI = new T(client); + } + +#if RAK_4631 + waitTime = 100; +#endif + return 100; // only check occasionally for incoming connections +} diff --git a/src/mesh/api/ServerAPI.h b/src/mesh/api/ServerAPI.h new file mode 100644 index 0000000..5b84fdd --- /dev/null +++ b/src/mesh/api/ServerAPI.h @@ -0,0 +1,56 @@ +#pragma once + +#include "StreamAPI.h" + +/** + * 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; + + public: + explicit ServerAPI(T &_client); + + virtual ~ServerAPI(); + + /// 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 {} + + 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; +}; + +/** + * 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; +#if RAK_4631 + // Track wait time for RAK13800 Ethernet requests + int32_t waitTime = 100; +#endif + + public: + explicit APIServerPort(int port); + + void init(); + + protected: + int32_t runOnce() override; +}; diff --git a/src/mesh/api/WiFiServerAPI.cpp b/src/mesh/api/WiFiServerAPI.cpp new file mode 100644 index 0000000..89ab2c6 --- /dev/null +++ b/src/mesh/api/WiFiServerAPI.cpp @@ -0,0 +1,29 @@ +#include "configuration.h" +#include + +#if HAS_WIFI +#include "WiFiServerAPI.h" + +static WiFiServerPort *apiPort; + +void initApiServer(int port) +{ + // Start API server on port 4403 + if (!apiPort) { + apiPort = new WiFiServerPort(port); + LOG_INFO("API server listening on TCP port %d", port); + apiPort->init(); + } +} +void deInitApiServer() +{ + delete apiPort; +} + +WiFiServerAPI::WiFiServerAPI(WiFiClient &_client) : ServerAPI(_client) +{ + LOG_INFO("Incoming wifi connection"); +} + +WiFiServerPort::WiFiServerPort(int port) : APIServerPort(port) {} +#endif \ No newline at end of file diff --git a/src/mesh/api/WiFiServerAPI.h b/src/mesh/api/WiFiServerAPI.h new file mode 100644 index 0000000..7a3d296 --- /dev/null +++ b/src/mesh/api/WiFiServerAPI.h @@ -0,0 +1,26 @@ +#pragma once + +#include "ServerAPI.h" +#include + +/** + * 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); +}; + +/** + * Listens for incoming connections and does accepts and creates instances of WiFiServerAPI as needed + */ +class WiFiServerPort : public APIServerPort +{ + public: + explicit WiFiServerPort(int port); +}; + +void initApiServer(int port = 4403); +void deInitApiServer(); \ No newline at end of file diff --git a/src/mesh/api/ethServerAPI.cpp b/src/mesh/api/ethServerAPI.cpp new file mode 100644 index 0000000..a870184 --- /dev/null +++ b/src/mesh/api/ethServerAPI.cpp @@ -0,0 +1,27 @@ +#include "configuration.h" +#include + +#if HAS_ETHERNET + +#include "ethServerAPI.h" + +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(); + } +} + +ethServerAPI::ethServerAPI(EthernetClient &_client) : ServerAPI(_client) +{ + LOG_INFO("Incoming ethernet connection"); +} + +ethServerPort::ethServerPort(int port) : APIServerPort(port) {} + +#endif \ No newline at end of file diff --git a/src/mesh/api/ethServerAPI.h b/src/mesh/api/ethServerAPI.h new file mode 100644 index 0000000..6f214c7 --- /dev/null +++ b/src/mesh/api/ethServerAPI.h @@ -0,0 +1,25 @@ +#pragma once + +#include "ServerAPI.h" +#include + +/** + * 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); +}; + +/** + * Listens for incoming connections and does accepts and creates instances of EthernetServerAPI as needed + */ +class ethServerPort : public APIServerPort +{ + public: + explicit ethServerPort(int port); +}; + +void initApiServer(int port = 4403); diff --git a/src/mesh/compression/unishox2.cpp b/src/mesh/compression/unishox2.cpp new file mode 100644 index 0000000..9fc012a --- /dev/null +++ b/src/mesh/compression/unishox2.cpp @@ -0,0 +1,1432 @@ +/* + * Copyright (C) 2020 Siara Logics (cc) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @author Arundale Ramanathan + * + * Port for Particle (particle.io) / Aruino - Jonathan Greenblatt + */ +/** + * @file unishox2.c + * @author Arundale Ramanathan, James Z. M. Gao + * @brief Main code of Unishox2 Compression and Decompression library + * + * This file implements the code for the Unishox API function \n + * defined in unishox2.h + */ + +#include +#include +#include +#include +#include + +#include "unishox2.h" + +/// uint8_t is unsigned char +typedef unsigned char uint8_t; + +const char *USX_FREQ_SEQ_DFLT[] = {"\": \"", "\": ", ""}; +const char *USX_FREQ_SEQ_XML[] = {"", "', ':', '\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 +/// Next 5 bits - position in usx_vcodes +uint8_t usx_code_94[94]; + +/// Vertical codes starting from the MSB +uint8_t usx_vcodes[] = {0x00, 0x40, 0x60, 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0, 0xD8, 0xE0, 0xE4, 0xE8, 0xEC, + 0xEE, 0xF0, 0xF2, 0xF4, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF}; + +/// 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 +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 +const int UTF8_MASK[] = {0xE0, 0xF0, 0xF8}; +/// Not used +const int UTF8_PREFIX[] = {0xC0, 0xE0, 0xF0}; + +/// Minimum length to consider as repeating sequence +#define NICE_LEN 5 + +/// Set (USX_NUM - 2) and vertical code (26) for encoding repeating letters +#define RPT_CODE ((2 << 5) + 26) +/// Set (USX_NUM - 2) and vertical code (27) for encoding terminator +#define TERM_CODE ((2 << 5) + 27) +/// Set (USX_SYM - 1) and vertical code (7) for encoding Line feed \\n +#define LF_CODE ((1 << 5) + 7) +/// Set (USX_NUM - 1) and vertical code (8) for encoding \\r\\n +#define CRLF_CODE ((1 << 5) + 8) +/// Set (USX_NUM - 1) and vertical code (22) for encoding \\r +#define CR_CODE ((1 << 5) + 22) +/// Set (USX_NUM - 1) and vertical code (14) for encoding \\t +#define TAB_CODE ((1 << 5) + 14) +/// Set (USX_NUM - 2) and vertical code (17) for space character when it appears in USX_NUM state \\r +#define NUM_SPC_CODE ((2 << 5) + 17) + +/// Code for special code (11111) when state=USX_DELTA +#define UNI_STATE_SPL_CODE 0xF8 +/// Length of Code for special code when state=USX_DELTA +#define UNI_STATE_SPL_CODE_LEN 5 +/// Code for switch code when state=USX_DELTA +#define UNI_STATE_SW_CODE 0x80 +/// Length of Code for Switch code when state=USX_DELTA +#define UNI_STATE_SW_CODE_LEN 2 + +/// Switch code in USX_ALPHA and USX_NUM 00 +#define SW_CODE 0 +/// Length of Switch code +#define SW_CODE_LEN 2 +/// Terminator bit sequence for Preset 1. Length varies depending on state as per following macros +#define TERM_BYTE_PRESET_1 0 +/// Length of Terminator bit sequence when state is lower +#define TERM_BYTE_PRESET_1_LEN_LOWER 6 +/// Length of Terminator bit sequence when state is upper +#define TERM_BYTE_PRESET_1_LEN_UPPER 4 + +/// Offset at which usx_code_94 starts +#define USX_OFFSET_94 33 + +/// global to indicate whether initialization is complete or not +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; + } + } + } + is_inited = 1; +} + +/// Mask for retrieving each code to be encoded according to its length +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) +{ + + // printf("%d,%x,%d,%d\n", ol, code, clen, state); + + 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; +} + +/// 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) + +/// 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; +} + +/// 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])); + return ol; +} + +/// Length of bits used to represent count for each level +const uint8_t count_bit_lens[5] = {2, 4, 7, 11, 16}; +/// Cumulative counts represented at each level +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; + } + } + return ol; +} + +/// Length of bits used to represent delta code for each level +const uint8_t uni_bit_len[5] = {6, 12, 14, 16, 21}; +/// Cumulative delta codes represented at each level +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; + } + } + 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; +} + +/// Finds the longest matching sequence from the beginning of the string. \n +/// If a match is found and it is longer than NICE_LEN, it is encoded as a repeating sequence to out \n +/// This is also used for Unicode strings \n +/// 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; + } + } + } + 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 +/// Finds the longest matching sequence from the previous array element to the beginning of the string array. \n +/// If a match is found and it is longer than NICE_LEN, it is encoded as a repeating sequence to out \n +/// This is also used for Unicode strings \n +/// 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; + } + } + 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; +} + +/// Enum indicating nibble type - USX_NIB_NUM means ch is a number '0' to '9', \n +/// USX_NIB_HEX_LOWER means ch is between 'a' to 'f', \n +/// USX_NIB_HEX_UPPER means ch is between 'A' to 'F' +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; +} + +/// 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; +} + +/// Returns minimum value of two longs +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)); + } + + // 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; +} + +/// 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) + +// 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) +{ + + uint8_t state; + + 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; +#else + 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++) { + + 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)); + } + } + 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; + } + } + } + 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])); + 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); +} + +// 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); +} + +// Reads one bit from in +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; +} + +/// The list of veritical codes is split into 5 sections. Used by readVCodeIdx() +#define SECTION_COUNT 5 +/// Used by readVCodeIdx() for finding the section under which the code read using read8bitCode() falls +uint8_t usx_vsections[] = {0x7F, 0xBF, 0xDF, 0xEF, 0xFF}; +/// Used by readVCodeIdx() for finding the section vertical position offset +uint8_t usx_vsection_pos[] = {0, 4, 8, 12, 20}; +/// Used by readVCodeIdx() for masking the code read by read8bitCode() +uint8_t usx_vsection_mask[] = {0x7F, 0x3F, 0x1F, 0x0F, 0x0F}; +/// Used by readVCodeIdx() for shifting the code read by read8bitCode() to obtain the vpos +uint8_t usx_vsection_shift[] = {5, 4, 3, 1, 0}; + +/// Vertical decoder lookup table - 3 bits code len, 5 bytes vertical pos +/// code len is one less as 8 cannot be accommodated in 3 bits +uint8_t usx_vcode_lookup[36] = {(1 << 5) + 0, (1 << 5) + 0, (2 << 5) + 1, (2 << 5) + 2, // Section 1 + (3 << 5) + 3, (3 << 5) + 4, (3 << 5) + 5, (3 << 5) + 6, // Section 2 + (3 << 5) + 7, (3 << 5) + 7, (4 << 5) + 8, (4 << 5) + 9, // Section 3 + (5 << 5) + 10, (5 << 5) + 10, (5 << 5) + 11, (5 << 5) + 11, // Section 4 + (5 << 5) + 12, (5 << 5) + 12, (6 << 5) + 13, (6 << 5) + 14, (6 << 5) + 15, (6 << 5) + 15, + (6 << 5) + 16, (6 << 5) + 16, // Section 5 + (6 << 5) + 17, (6 << 5) + 17, (7 << 5) + 18, (7 << 5) + 19, (7 << 5) + 20, (7 << 5) + 21, + (7 << 5) + 22, (7 << 5) + 23, (7 << 5) + 24, (7 << 5) + 25, (7 << 5) + 26, (7 << 5) + 27}; + +/// Decodes the vertical code from the given bitstream at in \n +/// This is designed to use less memory using a 36 uint8_t buffer \n +/// compared to using a 256 uint8_t buffer to decode the next 8 bits read by read8bitCode() \n +/// by splitting the list of vertical codes. \n +/// 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; +} + +/// Mask for retrieving each code to be decoded according to its length \n +/// Same as usx_mask so redundant +uint8_t len_masks[] = {0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC, 0xFE, 0xFF}; +/// Decodes the horizontal code from the given bitstream at in \n +/// 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; + } + } + } + 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; + (*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; +} + +/// 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; +} + +/// 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; +} + +/// 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) + +/// 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) + +/// 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; +} + +/// 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; +} + +/// 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; +} + +// 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) +{ + + 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; +#endif + + 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; + + 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; + 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 = 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; + } + } + } + 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; +} + +// 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); +} + +// 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); +} \ No newline at end of file diff --git a/src/mesh/compression/unishox2.h b/src/mesh/compression/unishox2.h new file mode 100644 index 0000000..5e2cc8b --- /dev/null +++ b/src/mesh/compression/unishox2.h @@ -0,0 +1,347 @@ +/* + * Copyright (C) 2020 Siara Logics (cc) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @author Arundale Ramanathan + * + * Port for Particle (particle.io) / Aruino - Jonathan Greenblatt + * + * This file describes each function of the Unishox2 API \n + * For finding out how this API can be used in your program, \n + * please see test_unishox2.c. + */ + +#ifndef unishox2 +#define unishox2 + +#define UNISHOX_VERSION "2.0" ///< Unicode spec version + +/** + * Macro switch to enable/disable output buffer length parameter in low level api \n + * Disabled by default \n + * When this macro is defined, the all the API functions \n + * except the simple API functions accept an additional parameter olen \n + * that enables the developer to pass the size of the output buffer provided \n + * so that the api function may not write beyond that length. \n + * This can be disabled if the developer knows that the buffer provided is sufficient enough \n + * so no additional parameter is passed and the program is faster since additional check \n + * for output length is not performed at each step \n + * The simple api, i.e. unishox2_(de)compress_simple will always omit the buffer length + */ +#ifndef UNISHOX_API_WITH_OUTPUT_LEN +#define UNISHOX_API_WITH_OUTPUT_LEN 1 +#endif + +/// Upto 8 bits of initial magic bit sequence can be included. Bit count can be specified with UNISHOX_MAGIC_BIT_LEN +#ifndef UNISHOX_MAGIC_BITS +#define UNISHOX_MAGIC_BITS 0xFF +#endif + +/// Desired length of Magic bits defined by UNISHOX_MAGIC_BITS +#ifdef UNISHOX_MAGIC_BIT_LEN +#if UNISHOX_MAGIC_BIT_LEN < 0 || 9 <= UNISHOX_MAGIC_BIT_LEN +#error "UNISHOX_MAGIC_BIT_LEN need between [0, 8)" +#endif +#else +#define UNISHOX_MAGIC_BIT_LEN 1 +#endif + +// 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 \ + } +/// Length of each default hcode +#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 \ + } +/// Length of each Alpha only hcode +#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 \ + } +/// Length of each Alpha numeric hcode +#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 \ + } +/// Length of each Alpha numeric and symbol hcodes +#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 \ + } +/// Length of each hcode favouring Alpha content +#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 \ + } +/// Length of each hcode favouring repeating sequences +#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 \ + } +/// Length of each hcode favouring symbols +#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 \ + } +/// Length of each hcode favouring umlaut letters +#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 \ + } +/// Length of each hcode for no repeating sequences +#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 \ + } +/// Length of each hcode for no Unicode characters +#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[]; +extern const char *USX_FREQ_SEQ_URL[]; +extern const char *USX_FREQ_SEQ_JSON[]; +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. +#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 +/// 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 +/// 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 +#define USX_PSET_FAVOR_DICT USX_HCODES_FAVOR_DICT, USX_HCODE_LENS_FAVOR_DICT, USX_FREQ_SEQ_DFLT, USX_TEMPLATES +/// Preset parameter set favouring symbols +#define USX_PSET_FAVOR_SYM USX_HCODES_FAVOR_SYM, USX_HCODE_LENS_FAVOR_SYM, USX_FREQ_SEQ_DFLT, USX_TEMPLATES +/// Preset parameter set favouring unlaut letters +#define USX_PSET_FAVOR_UMLAUT USX_HCODES_FAVOR_UMLAUT, USX_HCODE_LENS_FAVOR_UMLAUT, USX_FREQ_SEQ_DFLT, USX_TEMPLATES +/// Preset parameter set for when there are no repeating sequences +#define USX_PSET_NO_DICT USX_HCODES_NO_DICT, USX_HCODE_LENS_NO_DICT, USX_FREQ_SEQ_DFLT, USX_TEMPLATES +/// Preset parameter set for when there are no unicode symbols +#define USX_PSET_NO_UNI USX_HCODES_NO_UNI, USX_HCODE_LENS_NO_UNI, USX_FREQ_SEQ_DFLT, USX_TEMPLATES +/// Preset parameter set for when there are no unicode symbols favouring text +#define USX_PSET_NO_UNI_FAVOR_TEXT USX_HCODES_NO_UNI, USX_HCODE_LENS_NO_UNI, USX_FREQ_SEQ_TXT, USX_TEMPLATES +/// Preset parameter set favouring URL content +#define USX_PSET_URL USX_HCODES_DFLT, USX_HCODE_LENS_DFLT, USX_FREQ_SEQ_URL, USX_TEMPLATES +/// Preset parameter set favouring JSON content +#define USX_PSET_JSON USX_HCODES_DFLT, USX_HCODE_LENS_DFLT, USX_FREQ_SEQ_JSON, USX_TEMPLATES +/// Preset parameter set favouring JSON content having no Unicode symbols +#define USX_PSET_JSON_NO_UNI USX_HCODES_NO_UNI, USX_HCODE_LENS_NO_UNI, USX_FREQ_SEQ_JSON, USX_TEMPLATES +/// Preset parameter set favouring XML content +#define USX_PSET_XML USX_HCODES_DFLT, USX_HCODE_LENS_DFLT, USX_FREQ_SEQ_XML, USX_TEMPLATES +/// Preset parameter set favouring HTML content +#define USX_PSET_HTML USX_HCODES_DFLT, USX_HCODE_LENS_DFLT, USX_FREQ_SEQ_HTML, USX_TEMPLATES + +/** + * This structure is used when a string array needs to be compressed. + * This is passed as a parameter to the unishox2_decompress_lines() function + */ +struct us_lnk_lst { + char *data; + struct us_lnk_lst *previous; +}; + +/** + * This macro is for internal use, but builds upon the macro UNISHOX_API_WITH_OUTPUT_LEN + * When the macro UNISHOX_API_WITH_OUTPUT_LEN is defined, the all the API functions + * except the simple API functions accept an additional parameter olen + * that enables the developer to pass the size of the output buffer provided + * so that the api function may not write beyond that length. + * This can be disabled if the developer knows that the buffer provided is sufficient enough + * so no additional parameter is passed and the program is faster since additional check + * for output length is not performed at each step + */ +#if defined(UNISHOX_API_WITH_OUTPUT_LEN) && UNISHOX_API_WITH_OUTPUT_LEN != 0 +#define UNISHOX_API_OUT_AND_LEN(out, olen) out, olen +#else +#define UNISHOX_API_OUT_AND_LEN(out, olen) out +#endif + +/** + * Simple API for compressing a string + * @param[in] in Input ASCII / UTF-8 string + * @param[in] len length in bytes + * @param[out] out output buffer - should be large enough to hold compressed output + */ +extern int unishox2_compress_simple(const char *in, int len, char *out); +/** + * Simple API for decompressing a string + * @param[in] in Input compressed bytes (output of unishox2_compress functions) + * @param[in] len length of 'in' in bytes + * @param[out] out output buffer for ASCII / UTF-8 string - should be large enough + */ +extern int unishox2_decompress_simple(const char *in, int len, char *out); +/** + * Comprehensive API for compressing a string + * + * Presets are available for the last four parameters so they can be passed as single parameter. \n + * See USX_PSET_* macros. Example call: \n + * unishox2_compress(in, len, out, olen, USX_PSET_ALPHA_ONLY); + * + * @param[in] in Input ASCII / UTF-8 string + * @param[in] len length in bytes + * @param[out] out output buffer - should be large enough to hold compressed output + * @param[in] olen length of 'out' buffer in bytes. Can be omitted if sufficient buffer is provided + * @param[in] usx_hcodes Horizontal codes (array of bytes). See macro section for samples. + * @param[in] usx_hcode_lens Length of each element in usx_hcodes array + * @param[in] usx_freq_seq Frequently occuring sequences. See USX_FREQ_SEQ_* macros for samples + * @param[in] usx_templates Templates of frequently occuring 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[]); +/** + * Comprehensive API for de-compressing a string + * + * Presets are available for the last four parameters so they can be passed as single parameter. \n + * See USX_PSET_* macros. Example call: \n + * unishox2_decompress(in, len, out, olen, USX_PSET_ALPHA_ONLY); + * + * @param[in] in Input compressed bytes (output of unishox2_compress functions) + * @param[in] len length of 'in' in bytes + * @param[out] out output buffer - should be large enough to hold de-compressed output + * @param[in] olen length of 'out' buffer in bytes. Can be omitted if sufficient buffer is provided + * @param[in] usx_hcodes Horizontal codes (array of bytes). See macro section for samples. + * @param[in] usx_hcode_lens Length of each element in usx_hcodes array + * @param[in] usx_freq_seq Frequently occuring sequences. See USX_FREQ_SEQ_* macros for samples + * @param[in] usx_templates Templates of frequently occuring 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[]); +/** + * More Comprehensive API for compressing array of strings + * + * See unishox2_compress() function for parameter definitions. \n + * This function takes an additional parameter, i.e. 'prev_lines' - the usx_lnk_lst structure \n + * See -g parameter in test_unishox2.c to find out how this can be used. \n + * This function is used when an array of strings need to be compressed \n + * 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); +/** + * More Comprehensive API for de-compressing array of strings \n + * This function is not be used in conjuction with unishox2_compress_lines() + * + * See unishox2_decompress() function for parameter definitions. \n + * Typically an array is compressed using unishox2_compress_lines() and \n + * a header (.h) file is generated using the resultant compressed array. \n + * This header file can be used in another program with another decompress \n + * 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); + +#endif \ No newline at end of file diff --git a/src/mesh/eth/ethClient.cpp b/src/mesh/eth/ethClient.cpp new file mode 100644 index 0000000..67da224 --- /dev/null +++ b/src/mesh/eth/ethClient.cpp @@ -0,0 +1,190 @@ +#include "mesh/eth/ethClient.h" +#include "NodeDB.h" +#include "RTC.h" +#include "concurrency/Periodic.h" +#include "configuration.h" +#include "main.h" +#include "mesh/api/ethServerAPI.h" +#if !MESHTASTIC_EXCLUDE_MQTT +#include "mqtt/MQTT.h" +#endif +#include "target_specific.h" +#include +#include + +#if HAS_NETWORKING + +#ifndef DISABLE_NTP +#include + +// NTP +EthernetUDP ntpUDP; +NTPClient timeClient(ntpUDP, config.network.ntp_server); +uint32_t ntp_renew = 0; +#endif + +EthernetUDP syslogClient; +Syslog syslog(syslogClient); + +bool ethStartupComplete = 0; + +using namespace concurrency; + +static Periodic *ethEvent; + +static int32_t reconnectETH() +{ + if (config.network.eth_enabled) { + Ethernet.maintain(); + if (!ethStartupComplete) { + // Start web server + LOG_INFO("Starting Ethernet network services"); + +#ifndef DISABLE_NTP + LOG_INFO("Starting NTP time client"); + timeClient.begin(); + timeClient.setUpdateInterval(60 * 60); // Update once an hour +#endif + + if (config.network.rsyslog_server[0]) { + LOG_INFO("Starting 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(); + } + + // initWebServer(); + initApiServer(); + + ethStartupComplete = true; + } +#if !MESHTASTIC_EXCLUDE_MQTT + // FIXME this is kinda yucky, instead we should just have an observable for 'wifireconnected' + if (mqtt && !moduleConfig.mqtt.proxy_to_client_enabled && !mqtt->isConnectedDirectly()) { + mqtt->reconnect(); + } +#endif + } + +#ifndef DISABLE_NTP + if (isEthernetAvailable() && (ntp_renew < millis())) { + + LOG_INFO("Updating 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); + + 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 +} + +// Startup Ethernet +bool initEthernet() +{ + if (config.network.eth_enabled) { + +#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. +#endif + + Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); + + uint8_t mac[6]; + + int status = 0; + + // createSSLCert(); + + 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("starting Ethernet DHCP"); + status = Ethernet.begin(mac); + } else if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_STATIC) { + LOG_INFO("starting 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("Not using Ethernet"); + return false; + } +} + +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; + } +} + +#endif diff --git a/src/mesh/eth/ethClient.h b/src/mesh/eth/ethClient.h new file mode 100644 index 0000000..9e1745b --- /dev/null +++ b/src/mesh/eth/ethClient.h @@ -0,0 +1,8 @@ +#pragma once + +#include "configuration.h" +#include +#include + +bool initEthernet(); +bool isEthernetAvailable(); diff --git a/src/mesh/generated/.clang-format b/src/mesh/generated/.clang-format new file mode 100644 index 0000000..a43d914 --- /dev/null +++ b/src/mesh/generated/.clang-format @@ -0,0 +1,2 @@ +DisableFormat: true +SortIncludes: false \ No newline at end of file diff --git a/src/mesh/generated/meshtastic/admin.pb.cpp b/src/mesh/generated/meshtastic/admin.pb.cpp new file mode 100644 index 0000000..8b3fd3d --- /dev/null +++ b/src/mesh/generated/meshtastic/admin.pb.cpp @@ -0,0 +1,22 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9 */ + +#include "meshtastic/admin.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(meshtastic_AdminMessage, meshtastic_AdminMessage, 2) + + +PB_BIND(meshtastic_HamParameters, meshtastic_HamParameters, AUTO) + + +PB_BIND(meshtastic_NodeRemoteHardwarePinsResponse, meshtastic_NodeRemoteHardwarePinsResponse, 2) + + + + + + + diff --git a/src/mesh/generated/meshtastic/admin.pb.h b/src/mesh/generated/meshtastic/admin.pb.h new file mode 100644 index 0000000..d802eb3 --- /dev/null +++ b/src/mesh/generated/meshtastic/admin.pb.h @@ -0,0 +1,393 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9 */ + +#ifndef PB_MESHTASTIC_MESHTASTIC_ADMIN_PB_H_INCLUDED +#define PB_MESHTASTIC_MESHTASTIC_ADMIN_PB_H_INCLUDED +#include +#include "meshtastic/channel.pb.h" +#include "meshtastic/config.pb.h" +#include "meshtastic/connection_status.pb.h" +#include "meshtastic/mesh.pb.h" +#include "meshtastic/module_config.pb.h" +#include "meshtastic/device_ui.pb.h" + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Enum definitions */ +/* TODO: REPLACE */ +typedef enum _meshtastic_AdminMessage_ConfigType { + /* TODO: REPLACE */ + meshtastic_AdminMessage_ConfigType_DEVICE_CONFIG = 0, + /* TODO: REPLACE */ + meshtastic_AdminMessage_ConfigType_POSITION_CONFIG = 1, + /* TODO: REPLACE */ + meshtastic_AdminMessage_ConfigType_POWER_CONFIG = 2, + /* TODO: REPLACE */ + meshtastic_AdminMessage_ConfigType_NETWORK_CONFIG = 3, + /* TODO: REPLACE */ + meshtastic_AdminMessage_ConfigType_DISPLAY_CONFIG = 4, + /* TODO: REPLACE */ + meshtastic_AdminMessage_ConfigType_LORA_CONFIG = 5, + /* TODO: REPLACE */ + meshtastic_AdminMessage_ConfigType_BLUETOOTH_CONFIG = 6, + /* TODO: REPLACE */ + meshtastic_AdminMessage_ConfigType_SECURITY_CONFIG = 7, + /* */ + meshtastic_AdminMessage_ConfigType_SESSIONKEY_CONFIG = 8, + /* device-ui config */ + meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG = 9 +} meshtastic_AdminMessage_ConfigType; + +/* TODO: REPLACE */ +typedef enum _meshtastic_AdminMessage_ModuleConfigType { + /* TODO: REPLACE */ + meshtastic_AdminMessage_ModuleConfigType_MQTT_CONFIG = 0, + /* TODO: REPLACE */ + meshtastic_AdminMessage_ModuleConfigType_SERIAL_CONFIG = 1, + /* TODO: REPLACE */ + meshtastic_AdminMessage_ModuleConfigType_EXTNOTIF_CONFIG = 2, + /* TODO: REPLACE */ + meshtastic_AdminMessage_ModuleConfigType_STOREFORWARD_CONFIG = 3, + /* TODO: REPLACE */ + meshtastic_AdminMessage_ModuleConfigType_RANGETEST_CONFIG = 4, + /* TODO: REPLACE */ + meshtastic_AdminMessage_ModuleConfigType_TELEMETRY_CONFIG = 5, + /* TODO: REPLACE */ + meshtastic_AdminMessage_ModuleConfigType_CANNEDMSG_CONFIG = 6, + /* TODO: REPLACE */ + meshtastic_AdminMessage_ModuleConfigType_AUDIO_CONFIG = 7, + /* TODO: REPLACE */ + meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG = 8, + /* TODO: REPLACE */ + meshtastic_AdminMessage_ModuleConfigType_NEIGHBORINFO_CONFIG = 9, + /* TODO: REPLACE */ + meshtastic_AdminMessage_ModuleConfigType_AMBIENTLIGHTING_CONFIG = 10, + /* TODO: REPLACE */ + meshtastic_AdminMessage_ModuleConfigType_DETECTIONSENSOR_CONFIG = 11, + /* TODO: REPLACE */ + meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG = 12 +} meshtastic_AdminMessage_ModuleConfigType; + +/* Struct definitions */ +/* Parameters for setting up Meshtastic for ameteur radio usage */ +typedef struct _meshtastic_HamParameters { + /* Amateur radio call sign, eg. KD2ABC */ + char call_sign[8]; + /* Transmit power in dBm at the LoRA transceiver, not including any amplification */ + int32_t tx_power; + /* The selected frequency of LoRA operation + Please respect your local laws, regulations, and band plans. + Ensure your radio is capable of operating of the selected frequency before setting this. */ + float frequency; + /* Optional short name of user */ + char short_name[5]; +} meshtastic_HamParameters; + +/* Response envelope for node_remote_hardware_pins */ +typedef struct _meshtastic_NodeRemoteHardwarePinsResponse { + /* Nodes and their respective remote hardware GPIO pins */ + pb_size_t node_remote_hardware_pins_count; + meshtastic_NodeRemoteHardwarePin node_remote_hardware_pins[16]; +} meshtastic_NodeRemoteHardwarePinsResponse; + +typedef PB_BYTES_ARRAY_T(8) meshtastic_AdminMessage_session_passkey_t; +/* This message is handled by the Admin module and is responsible for all settings/channel read/write operations. + This message is used to do settings operations to both remote AND local nodes. + (Prior to 1.2 these operations were done via special ToRadio operations) */ +typedef struct _meshtastic_AdminMessage { + pb_size_t which_payload_variant; + union { + /* Send the specified channel in the response to this message + NOTE: This field is sent with the channel index + 1 (to ensure we never try to send 'zero' - which protobufs treats as not present) */ + uint32_t get_channel_request; + /* TODO: REPLACE */ + meshtastic_Channel get_channel_response; + /* Send the current owner data in the response to this message. */ + bool get_owner_request; + /* TODO: REPLACE */ + meshtastic_User get_owner_response; + /* Ask for the following config data to be sent */ + meshtastic_AdminMessage_ConfigType get_config_request; + /* Send the current Config in the response to this message. */ + meshtastic_Config get_config_response; + /* Ask for the following config data to be sent */ + meshtastic_AdminMessage_ModuleConfigType get_module_config_request; + /* Send the current Config in the response to this message. */ + meshtastic_ModuleConfig get_module_config_response; + /* Get the Canned Message Module messages in the response to this message. */ + bool get_canned_message_module_messages_request; + /* Get the Canned Message Module messages in the response to this message. */ + char get_canned_message_module_messages_response[201]; + /* Request the node to send device metadata (firmware, protobuf version, etc) */ + bool get_device_metadata_request; + /* Device metadata response */ + meshtastic_DeviceMetadata get_device_metadata_response; + /* Get the Ringtone in the response to this message. */ + bool get_ringtone_request; + /* Get the Ringtone in the response to this message. */ + char get_ringtone_response[231]; + /* Request the node to send it's connection status */ + bool get_device_connection_status_request; + /* Device connection status response */ + meshtastic_DeviceConnectionStatus get_device_connection_status_response; + /* Setup a node for licensed amateur (ham) radio operation */ + meshtastic_HamParameters set_ham_mode; + /* Get the mesh's nodes with their available gpio pins for RemoteHardware module use */ + bool get_node_remote_hardware_pins_request; + /* Respond with the mesh's nodes with their available gpio pins for RemoteHardware module use */ + meshtastic_NodeRemoteHardwarePinsResponse get_node_remote_hardware_pins_response; + /* Enter (UF2) DFU mode + Only implemented on NRF52 currently */ + bool enter_dfu_mode_request; + /* Delete the file by the specified path from the device */ + char delete_file_request[201]; + /* Set zero and offset for scale chips */ + uint32_t set_scale; + /* Set the owner for this node */ + meshtastic_User set_owner; + /* Set channels (using the new API). + A special channel is the "primary channel". + The other records are secondary channels. + Note: only one channel can be marked as primary. + If the client sets a particular channel to be primary, the previous channel will be set to SECONDARY automatically. */ + meshtastic_Channel set_channel; + /* Set the current Config */ + meshtastic_Config set_config; + /* Set the current Config */ + meshtastic_ModuleConfig set_module_config; + /* Set the Canned Message Module messages text. */ + char set_canned_message_module_messages[201]; + /* Set the ringtone for ExternalNotification. */ + char set_ringtone_message[231]; + /* Remove the node by the specified node-num from the NodeDB on the device */ + uint32_t remove_by_nodenum; + /* Set specified node-num to be favorited on the NodeDB on the device */ + uint32_t set_favorite_node; + /* Set specified node-num to be un-favorited on the NodeDB on the device */ + uint32_t remove_favorite_node; + /* Set fixed position data on the node and then set the position.fixed_position = true */ + meshtastic_Position set_fixed_position; + /* Clear fixed position coordinates and then set position.fixed_position = false */ + bool remove_fixed_position; + /* Set time only on the node + Convenience method to set the time on the node (as Net quality) without any other position data */ + uint32_t set_time_only; + /* Tell the node to send the stored ui data. */ + bool get_ui_config_request; + /* Reply stored device ui data. */ + meshtastic_DeviceUIConfig get_ui_config_response; + /* Tell the node to store UI data persistently. */ + meshtastic_DeviceUIConfig store_ui_config; + /* Begins an edit transaction for config, module config, owner, and channel settings changes + This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings) */ + bool begin_edit_settings; + /* Commits an open transaction for any edits made to config, module config, owner, and channel settings */ + bool commit_edit_settings; + /* Tell the node to factory reset config everything; all device state and configuration will be returned to factory defaults and BLE bonds will be cleared. */ + int32_t factory_reset_device; + /* Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot) + Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth. */ + int32_t reboot_ota_seconds; + /* This message is only supported for the simulator Portduino build. + If received the simulator will exit successfully. */ + bool exit_simulator; + /* Tell the node to reboot in this many seconds (or <0 to cancel reboot) */ + int32_t reboot_seconds; + /* Tell the node to shutdown in this many seconds (or <0 to cancel shutdown) */ + int32_t shutdown_seconds; + /* Tell the node to factory reset config; all device state and configuration will be returned to factory defaults; BLE bonds will be preserved. */ + int32_t factory_reset_config; + /* Tell the node to reset the nodedb. */ + int32_t nodedb_reset; + }; + /* The node generates this key and sends it with any get_x_response packets. + The client MUST include the same key with any set_x commands. Key expires after 300 seconds. + Prevents replay attacks for admin messages. */ + meshtastic_AdminMessage_session_passkey_t session_passkey; +} meshtastic_AdminMessage; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Helper constants for enums */ +#define _meshtastic_AdminMessage_ConfigType_MIN meshtastic_AdminMessage_ConfigType_DEVICE_CONFIG +#define _meshtastic_AdminMessage_ConfigType_MAX meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG +#define _meshtastic_AdminMessage_ConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ConfigType)(meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG+1)) + +#define _meshtastic_AdminMessage_ModuleConfigType_MIN meshtastic_AdminMessage_ModuleConfigType_MQTT_CONFIG +#define _meshtastic_AdminMessage_ModuleConfigType_MAX meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG +#define _meshtastic_AdminMessage_ModuleConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ModuleConfigType)(meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG+1)) + +#define meshtastic_AdminMessage_payload_variant_get_config_request_ENUMTYPE meshtastic_AdminMessage_ConfigType +#define meshtastic_AdminMessage_payload_variant_get_module_config_request_ENUMTYPE meshtastic_AdminMessage_ModuleConfigType + + + + +/* Initializer values for message structs */ +#define meshtastic_AdminMessage_init_default {0, {0}, {0, {0}}} +#define meshtastic_HamParameters_init_default {"", 0, 0, ""} +#define meshtastic_NodeRemoteHardwarePinsResponse_init_default {0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}} +#define meshtastic_AdminMessage_init_zero {0, {0}, {0, {0}}} +#define meshtastic_HamParameters_init_zero {"", 0, 0, ""} +#define meshtastic_NodeRemoteHardwarePinsResponse_init_zero {0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}} + +/* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_HamParameters_call_sign_tag 1 +#define meshtastic_HamParameters_tx_power_tag 2 +#define meshtastic_HamParameters_frequency_tag 3 +#define meshtastic_HamParameters_short_name_tag 4 +#define meshtastic_NodeRemoteHardwarePinsResponse_node_remote_hardware_pins_tag 1 +#define meshtastic_AdminMessage_get_channel_request_tag 1 +#define meshtastic_AdminMessage_get_channel_response_tag 2 +#define meshtastic_AdminMessage_get_owner_request_tag 3 +#define meshtastic_AdminMessage_get_owner_response_tag 4 +#define meshtastic_AdminMessage_get_config_request_tag 5 +#define meshtastic_AdminMessage_get_config_response_tag 6 +#define meshtastic_AdminMessage_get_module_config_request_tag 7 +#define meshtastic_AdminMessage_get_module_config_response_tag 8 +#define meshtastic_AdminMessage_get_canned_message_module_messages_request_tag 10 +#define meshtastic_AdminMessage_get_canned_message_module_messages_response_tag 11 +#define meshtastic_AdminMessage_get_device_metadata_request_tag 12 +#define meshtastic_AdminMessage_get_device_metadata_response_tag 13 +#define meshtastic_AdminMessage_get_ringtone_request_tag 14 +#define meshtastic_AdminMessage_get_ringtone_response_tag 15 +#define meshtastic_AdminMessage_get_device_connection_status_request_tag 16 +#define meshtastic_AdminMessage_get_device_connection_status_response_tag 17 +#define meshtastic_AdminMessage_set_ham_mode_tag 18 +#define meshtastic_AdminMessage_get_node_remote_hardware_pins_request_tag 19 +#define meshtastic_AdminMessage_get_node_remote_hardware_pins_response_tag 20 +#define meshtastic_AdminMessage_enter_dfu_mode_request_tag 21 +#define meshtastic_AdminMessage_delete_file_request_tag 22 +#define meshtastic_AdminMessage_set_scale_tag 23 +#define meshtastic_AdminMessage_set_owner_tag 32 +#define meshtastic_AdminMessage_set_channel_tag 33 +#define meshtastic_AdminMessage_set_config_tag 34 +#define meshtastic_AdminMessage_set_module_config_tag 35 +#define meshtastic_AdminMessage_set_canned_message_module_messages_tag 36 +#define meshtastic_AdminMessage_set_ringtone_message_tag 37 +#define meshtastic_AdminMessage_remove_by_nodenum_tag 38 +#define meshtastic_AdminMessage_set_favorite_node_tag 39 +#define meshtastic_AdminMessage_remove_favorite_node_tag 40 +#define meshtastic_AdminMessage_set_fixed_position_tag 41 +#define meshtastic_AdminMessage_remove_fixed_position_tag 42 +#define meshtastic_AdminMessage_set_time_only_tag 43 +#define meshtastic_AdminMessage_get_ui_config_request_tag 44 +#define meshtastic_AdminMessage_get_ui_config_response_tag 45 +#define meshtastic_AdminMessage_store_ui_config_tag 46 +#define meshtastic_AdminMessage_begin_edit_settings_tag 64 +#define meshtastic_AdminMessage_commit_edit_settings_tag 65 +#define meshtastic_AdminMessage_factory_reset_device_tag 94 +#define meshtastic_AdminMessage_reboot_ota_seconds_tag 95 +#define meshtastic_AdminMessage_exit_simulator_tag 96 +#define meshtastic_AdminMessage_reboot_seconds_tag 97 +#define meshtastic_AdminMessage_shutdown_seconds_tag 98 +#define meshtastic_AdminMessage_factory_reset_config_tag 99 +#define meshtastic_AdminMessage_nodedb_reset_tag 100 +#define meshtastic_AdminMessage_session_passkey_tag 101 + +/* Struct field encoding specification for nanopb */ +#define meshtastic_AdminMessage_FIELDLIST(X, a) \ +X(a, STATIC, ONEOF, UINT32, (payload_variant,get_channel_request,get_channel_request), 1) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,get_channel_response,get_channel_response), 2) \ +X(a, STATIC, ONEOF, BOOL, (payload_variant,get_owner_request,get_owner_request), 3) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,get_owner_response,get_owner_response), 4) \ +X(a, STATIC, ONEOF, UENUM, (payload_variant,get_config_request,get_config_request), 5) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,get_config_response,get_config_response), 6) \ +X(a, STATIC, ONEOF, UENUM, (payload_variant,get_module_config_request,get_module_config_request), 7) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,get_module_config_response,get_module_config_response), 8) \ +X(a, STATIC, ONEOF, BOOL, (payload_variant,get_canned_message_module_messages_request,get_canned_message_module_messages_request), 10) \ +X(a, STATIC, ONEOF, STRING, (payload_variant,get_canned_message_module_messages_response,get_canned_message_module_messages_response), 11) \ +X(a, STATIC, ONEOF, BOOL, (payload_variant,get_device_metadata_request,get_device_metadata_request), 12) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,get_device_metadata_response,get_device_metadata_response), 13) \ +X(a, STATIC, ONEOF, BOOL, (payload_variant,get_ringtone_request,get_ringtone_request), 14) \ +X(a, STATIC, ONEOF, STRING, (payload_variant,get_ringtone_response,get_ringtone_response), 15) \ +X(a, STATIC, ONEOF, BOOL, (payload_variant,get_device_connection_status_request,get_device_connection_status_request), 16) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,get_device_connection_status_response,get_device_connection_status_response), 17) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,set_ham_mode,set_ham_mode), 18) \ +X(a, STATIC, ONEOF, BOOL, (payload_variant,get_node_remote_hardware_pins_request,get_node_remote_hardware_pins_request), 19) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,get_node_remote_hardware_pins_response,get_node_remote_hardware_pins_response), 20) \ +X(a, STATIC, ONEOF, BOOL, (payload_variant,enter_dfu_mode_request,enter_dfu_mode_request), 21) \ +X(a, STATIC, ONEOF, STRING, (payload_variant,delete_file_request,delete_file_request), 22) \ +X(a, STATIC, ONEOF, UINT32, (payload_variant,set_scale,set_scale), 23) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,set_owner,set_owner), 32) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,set_channel,set_channel), 33) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,set_config,set_config), 34) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,set_module_config,set_module_config), 35) \ +X(a, STATIC, ONEOF, STRING, (payload_variant,set_canned_message_module_messages,set_canned_message_module_messages), 36) \ +X(a, STATIC, ONEOF, STRING, (payload_variant,set_ringtone_message,set_ringtone_message), 37) \ +X(a, STATIC, ONEOF, UINT32, (payload_variant,remove_by_nodenum,remove_by_nodenum), 38) \ +X(a, STATIC, ONEOF, UINT32, (payload_variant,set_favorite_node,set_favorite_node), 39) \ +X(a, STATIC, ONEOF, UINT32, (payload_variant,remove_favorite_node,remove_favorite_node), 40) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,set_fixed_position,set_fixed_position), 41) \ +X(a, STATIC, ONEOF, BOOL, (payload_variant,remove_fixed_position,remove_fixed_position), 42) \ +X(a, STATIC, ONEOF, FIXED32, (payload_variant,set_time_only,set_time_only), 43) \ +X(a, STATIC, ONEOF, BOOL, (payload_variant,get_ui_config_request,get_ui_config_request), 44) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,get_ui_config_response,get_ui_config_response), 45) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,store_ui_config,store_ui_config), 46) \ +X(a, STATIC, ONEOF, BOOL, (payload_variant,begin_edit_settings,begin_edit_settings), 64) \ +X(a, STATIC, ONEOF, BOOL, (payload_variant,commit_edit_settings,commit_edit_settings), 65) \ +X(a, STATIC, ONEOF, INT32, (payload_variant,factory_reset_device,factory_reset_device), 94) \ +X(a, STATIC, ONEOF, INT32, (payload_variant,reboot_ota_seconds,reboot_ota_seconds), 95) \ +X(a, STATIC, ONEOF, BOOL, (payload_variant,exit_simulator,exit_simulator), 96) \ +X(a, STATIC, ONEOF, INT32, (payload_variant,reboot_seconds,reboot_seconds), 97) \ +X(a, STATIC, ONEOF, INT32, (payload_variant,shutdown_seconds,shutdown_seconds), 98) \ +X(a, STATIC, ONEOF, INT32, (payload_variant,factory_reset_config,factory_reset_config), 99) \ +X(a, STATIC, ONEOF, INT32, (payload_variant,nodedb_reset,nodedb_reset), 100) \ +X(a, STATIC, SINGULAR, BYTES, session_passkey, 101) +#define meshtastic_AdminMessage_CALLBACK NULL +#define meshtastic_AdminMessage_DEFAULT NULL +#define meshtastic_AdminMessage_payload_variant_get_channel_response_MSGTYPE meshtastic_Channel +#define meshtastic_AdminMessage_payload_variant_get_owner_response_MSGTYPE meshtastic_User +#define meshtastic_AdminMessage_payload_variant_get_config_response_MSGTYPE meshtastic_Config +#define meshtastic_AdminMessage_payload_variant_get_module_config_response_MSGTYPE meshtastic_ModuleConfig +#define meshtastic_AdminMessage_payload_variant_get_device_metadata_response_MSGTYPE meshtastic_DeviceMetadata +#define meshtastic_AdminMessage_payload_variant_get_device_connection_status_response_MSGTYPE meshtastic_DeviceConnectionStatus +#define meshtastic_AdminMessage_payload_variant_set_ham_mode_MSGTYPE meshtastic_HamParameters +#define meshtastic_AdminMessage_payload_variant_get_node_remote_hardware_pins_response_MSGTYPE meshtastic_NodeRemoteHardwarePinsResponse +#define meshtastic_AdminMessage_payload_variant_set_owner_MSGTYPE meshtastic_User +#define meshtastic_AdminMessage_payload_variant_set_channel_MSGTYPE meshtastic_Channel +#define meshtastic_AdminMessage_payload_variant_set_config_MSGTYPE meshtastic_Config +#define meshtastic_AdminMessage_payload_variant_set_module_config_MSGTYPE meshtastic_ModuleConfig +#define meshtastic_AdminMessage_payload_variant_set_fixed_position_MSGTYPE meshtastic_Position +#define meshtastic_AdminMessage_payload_variant_get_ui_config_response_MSGTYPE meshtastic_DeviceUIConfig +#define meshtastic_AdminMessage_payload_variant_store_ui_config_MSGTYPE meshtastic_DeviceUIConfig + +#define meshtastic_HamParameters_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, STRING, call_sign, 1) \ +X(a, STATIC, SINGULAR, INT32, tx_power, 2) \ +X(a, STATIC, SINGULAR, FLOAT, frequency, 3) \ +X(a, STATIC, SINGULAR, STRING, short_name, 4) +#define meshtastic_HamParameters_CALLBACK NULL +#define meshtastic_HamParameters_DEFAULT NULL + +#define meshtastic_NodeRemoteHardwarePinsResponse_FIELDLIST(X, a) \ +X(a, STATIC, REPEATED, MESSAGE, node_remote_hardware_pins, 1) +#define meshtastic_NodeRemoteHardwarePinsResponse_CALLBACK NULL +#define meshtastic_NodeRemoteHardwarePinsResponse_DEFAULT NULL +#define meshtastic_NodeRemoteHardwarePinsResponse_node_remote_hardware_pins_MSGTYPE meshtastic_NodeRemoteHardwarePin + +extern const pb_msgdesc_t meshtastic_AdminMessage_msg; +extern const pb_msgdesc_t meshtastic_HamParameters_msg; +extern const pb_msgdesc_t meshtastic_NodeRemoteHardwarePinsResponse_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define meshtastic_AdminMessage_fields &meshtastic_AdminMessage_msg +#define meshtastic_HamParameters_fields &meshtastic_HamParameters_msg +#define meshtastic_NodeRemoteHardwarePinsResponse_fields &meshtastic_NodeRemoteHardwarePinsResponse_msg + +/* Maximum encoded size of messages (where known) */ +#define MESHTASTIC_MESHTASTIC_ADMIN_PB_H_MAX_SIZE meshtastic_AdminMessage_size +#define meshtastic_AdminMessage_size 511 +#define meshtastic_HamParameters_size 31 +#define meshtastic_NodeRemoteHardwarePinsResponse_size 496 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/mesh/generated/meshtastic/apponly.pb.cpp b/src/mesh/generated/meshtastic/apponly.pb.cpp new file mode 100644 index 0000000..64d43b7 --- /dev/null +++ b/src/mesh/generated/meshtastic/apponly.pb.cpp @@ -0,0 +1,12 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9 */ + +#include "meshtastic/apponly.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(meshtastic_ChannelSet, meshtastic_ChannelSet, 2) + + + diff --git a/src/mesh/generated/meshtastic/apponly.pb.h b/src/mesh/generated/meshtastic/apponly.pb.h new file mode 100644 index 0000000..dc08d9f --- /dev/null +++ b/src/mesh/generated/meshtastic/apponly.pb.h @@ -0,0 +1,64 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9 */ + +#ifndef PB_MESHTASTIC_MESHTASTIC_APPONLY_PB_H_INCLUDED +#define PB_MESHTASTIC_MESHTASTIC_APPONLY_PB_H_INCLUDED +#include +#include "meshtastic/channel.pb.h" +#include "meshtastic/config.pb.h" + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Struct definitions */ +/* This is the most compact possible representation for a set of channels. + It includes only one PRIMARY channel (which must be first) and + any SECONDARY channels. + No DISABLED channels are included. + This abstraction is used only on the the 'app side' of the world (ie python, javascript and android etc) to show a group of Channels as a (long) URL */ +typedef struct _meshtastic_ChannelSet { + /* Channel list with settings */ + pb_size_t settings_count; + meshtastic_ChannelSettings settings[8]; + /* LoRa config */ + bool has_lora_config; + meshtastic_Config_LoRaConfig lora_config; +} meshtastic_ChannelSet; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initializer values for message structs */ +#define meshtastic_ChannelSet_init_default {0, {meshtastic_ChannelSettings_init_default, meshtastic_ChannelSettings_init_default, meshtastic_ChannelSettings_init_default, meshtastic_ChannelSettings_init_default, meshtastic_ChannelSettings_init_default, meshtastic_ChannelSettings_init_default, meshtastic_ChannelSettings_init_default, meshtastic_ChannelSettings_init_default}, false, meshtastic_Config_LoRaConfig_init_default} +#define meshtastic_ChannelSet_init_zero {0, {meshtastic_ChannelSettings_init_zero, meshtastic_ChannelSettings_init_zero, meshtastic_ChannelSettings_init_zero, meshtastic_ChannelSettings_init_zero, meshtastic_ChannelSettings_init_zero, meshtastic_ChannelSettings_init_zero, meshtastic_ChannelSettings_init_zero, meshtastic_ChannelSettings_init_zero}, false, meshtastic_Config_LoRaConfig_init_zero} + +/* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_ChannelSet_settings_tag 1 +#define meshtastic_ChannelSet_lora_config_tag 2 + +/* Struct field encoding specification for nanopb */ +#define meshtastic_ChannelSet_FIELDLIST(X, a) \ +X(a, STATIC, REPEATED, MESSAGE, settings, 1) \ +X(a, STATIC, OPTIONAL, MESSAGE, lora_config, 2) +#define meshtastic_ChannelSet_CALLBACK NULL +#define meshtastic_ChannelSet_DEFAULT NULL +#define meshtastic_ChannelSet_settings_MSGTYPE meshtastic_ChannelSettings +#define meshtastic_ChannelSet_lora_config_MSGTYPE meshtastic_Config_LoRaConfig + +extern const pb_msgdesc_t meshtastic_ChannelSet_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define meshtastic_ChannelSet_fields &meshtastic_ChannelSet_msg + +/* Maximum encoded size of messages (where known) */ +#define MESHTASTIC_MESHTASTIC_APPONLY_PB_H_MAX_SIZE meshtastic_ChannelSet_size +#define meshtastic_ChannelSet_size 679 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/mesh/generated/meshtastic/atak.pb.cpp b/src/mesh/generated/meshtastic/atak.pb.cpp new file mode 100644 index 0000000..6dbc69f --- /dev/null +++ b/src/mesh/generated/meshtastic/atak.pb.cpp @@ -0,0 +1,31 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9 */ + +#include "meshtastic/atak.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(meshtastic_TAKPacket, meshtastic_TAKPacket, 2) + + +PB_BIND(meshtastic_GeoChat, meshtastic_GeoChat, 2) + + +PB_BIND(meshtastic_Group, meshtastic_Group, AUTO) + + +PB_BIND(meshtastic_Status, meshtastic_Status, AUTO) + + +PB_BIND(meshtastic_Contact, meshtastic_Contact, AUTO) + + +PB_BIND(meshtastic_PLI, meshtastic_PLI, AUTO) + + + + + + + diff --git a/src/mesh/generated/meshtastic/atak.pb.h b/src/mesh/generated/meshtastic/atak.pb.h new file mode 100644 index 0000000..15a8678 --- /dev/null +++ b/src/mesh/generated/meshtastic/atak.pb.h @@ -0,0 +1,286 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9 */ + +#ifndef PB_MESHTASTIC_MESHTASTIC_ATAK_PB_H_INCLUDED +#define PB_MESHTASTIC_MESHTASTIC_ATAK_PB_H_INCLUDED +#include + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Enum definitions */ +typedef enum _meshtastic_Team { + /* Unspecifed */ + meshtastic_Team_Unspecifed_Color = 0, + /* White */ + meshtastic_Team_White = 1, + /* Yellow */ + meshtastic_Team_Yellow = 2, + /* Orange */ + meshtastic_Team_Orange = 3, + /* Magenta */ + meshtastic_Team_Magenta = 4, + /* Red */ + meshtastic_Team_Red = 5, + /* Maroon */ + meshtastic_Team_Maroon = 6, + /* Purple */ + meshtastic_Team_Purple = 7, + /* Dark Blue */ + meshtastic_Team_Dark_Blue = 8, + /* Blue */ + meshtastic_Team_Blue = 9, + /* Cyan */ + meshtastic_Team_Cyan = 10, + /* Teal */ + meshtastic_Team_Teal = 11, + /* Green */ + meshtastic_Team_Green = 12, + /* Dark Green */ + meshtastic_Team_Dark_Green = 13, + /* Brown */ + meshtastic_Team_Brown = 14 +} meshtastic_Team; + +/* Role of the group member */ +typedef enum _meshtastic_MemberRole { + /* Unspecifed */ + meshtastic_MemberRole_Unspecifed = 0, + /* Team Member */ + meshtastic_MemberRole_TeamMember = 1, + /* Team Lead */ + meshtastic_MemberRole_TeamLead = 2, + /* Headquarters */ + meshtastic_MemberRole_HQ = 3, + /* Airsoft enthusiast */ + meshtastic_MemberRole_Sniper = 4, + /* Medic */ + meshtastic_MemberRole_Medic = 5, + /* ForwardObserver */ + meshtastic_MemberRole_ForwardObserver = 6, + /* Radio Telephone Operator */ + meshtastic_MemberRole_RTO = 7, + /* Doggo */ + meshtastic_MemberRole_K9 = 8 +} meshtastic_MemberRole; + +/* Struct definitions */ +/* ATAK GeoChat message */ +typedef struct _meshtastic_GeoChat { + /* The text message */ + char message[200]; + /* Uid recipient of the message */ + bool has_to; + char to[120]; + /* Callsign of the recipient for the message */ + bool has_to_callsign; + char to_callsign[120]; +} meshtastic_GeoChat; + +/* ATAK Group + <__group role='Team Member' name='Cyan'/> */ +typedef struct _meshtastic_Group { + /* Role of the group member */ + meshtastic_MemberRole role; + /* Team (color) + Default Cyan */ + meshtastic_Team team; +} meshtastic_Group; + +/* ATAK EUD Status + */ +typedef struct _meshtastic_Status { + /* Battery level */ + uint8_t battery; +} meshtastic_Status; + +/* ATAK Contact + */ +typedef struct _meshtastic_Contact { + /* Callsign */ + char callsign[120]; + /* Device callsign */ + char device_callsign[120]; /* IP address of endpoint in integer form (0.0.0.0 default) */ +} meshtastic_Contact; + +/* Position Location Information from ATAK */ +typedef struct _meshtastic_PLI { + /* The new preferred location encoding, multiply by 1e-7 to get degrees + in floating point */ + int32_t latitude_i; + /* The new preferred location encoding, multiply by 1e-7 to get degrees + in floating point */ + int32_t longitude_i; + /* Altitude (ATAK prefers HAE) */ + int32_t altitude; + /* Speed */ + uint32_t speed; + /* Course in degrees */ + uint16_t course; +} meshtastic_PLI; + +typedef PB_BYTES_ARRAY_T(220) meshtastic_TAKPacket_detail_t; +/* Packets for the official ATAK Plugin */ +typedef struct _meshtastic_TAKPacket { + /* Are the payloads strings compressed for LoRA transport? */ + bool is_compressed; + /* The contact / callsign for ATAK user */ + bool has_contact; + meshtastic_Contact contact; + /* The group for ATAK user */ + bool has_group; + meshtastic_Group group; + /* The status of the ATAK EUD */ + bool has_status; + meshtastic_Status status; + pb_size_t which_payload_variant; + union { + /* TAK position report */ + meshtastic_PLI pli; + /* ATAK GeoChat message */ + meshtastic_GeoChat chat; + /* Generic CoT detail XML + May be compressed / truncated by the sender (EUD) */ + meshtastic_TAKPacket_detail_t detail; + } payload_variant; +} meshtastic_TAKPacket; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Helper constants for enums */ +#define _meshtastic_Team_MIN meshtastic_Team_Unspecifed_Color +#define _meshtastic_Team_MAX meshtastic_Team_Brown +#define _meshtastic_Team_ARRAYSIZE ((meshtastic_Team)(meshtastic_Team_Brown+1)) + +#define _meshtastic_MemberRole_MIN meshtastic_MemberRole_Unspecifed +#define _meshtastic_MemberRole_MAX meshtastic_MemberRole_K9 +#define _meshtastic_MemberRole_ARRAYSIZE ((meshtastic_MemberRole)(meshtastic_MemberRole_K9+1)) + + + +#define meshtastic_Group_role_ENUMTYPE meshtastic_MemberRole +#define meshtastic_Group_team_ENUMTYPE meshtastic_Team + + + + + +/* Initializer values for message structs */ +#define meshtastic_TAKPacket_init_default {0, false, meshtastic_Contact_init_default, false, meshtastic_Group_init_default, false, meshtastic_Status_init_default, 0, {meshtastic_PLI_init_default}} +#define meshtastic_GeoChat_init_default {"", false, "", false, ""} +#define meshtastic_Group_init_default {_meshtastic_MemberRole_MIN, _meshtastic_Team_MIN} +#define meshtastic_Status_init_default {0} +#define meshtastic_Contact_init_default {"", ""} +#define meshtastic_PLI_init_default {0, 0, 0, 0, 0} +#define meshtastic_TAKPacket_init_zero {0, false, meshtastic_Contact_init_zero, false, meshtastic_Group_init_zero, false, meshtastic_Status_init_zero, 0, {meshtastic_PLI_init_zero}} +#define meshtastic_GeoChat_init_zero {"", false, "", false, ""} +#define meshtastic_Group_init_zero {_meshtastic_MemberRole_MIN, _meshtastic_Team_MIN} +#define meshtastic_Status_init_zero {0} +#define meshtastic_Contact_init_zero {"", ""} +#define meshtastic_PLI_init_zero {0, 0, 0, 0, 0} + +/* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_GeoChat_message_tag 1 +#define meshtastic_GeoChat_to_tag 2 +#define meshtastic_GeoChat_to_callsign_tag 3 +#define meshtastic_Group_role_tag 1 +#define meshtastic_Group_team_tag 2 +#define meshtastic_Status_battery_tag 1 +#define meshtastic_Contact_callsign_tag 1 +#define meshtastic_Contact_device_callsign_tag 2 +#define meshtastic_PLI_latitude_i_tag 1 +#define meshtastic_PLI_longitude_i_tag 2 +#define meshtastic_PLI_altitude_tag 3 +#define meshtastic_PLI_speed_tag 4 +#define meshtastic_PLI_course_tag 5 +#define meshtastic_TAKPacket_is_compressed_tag 1 +#define meshtastic_TAKPacket_contact_tag 2 +#define meshtastic_TAKPacket_group_tag 3 +#define meshtastic_TAKPacket_status_tag 4 +#define meshtastic_TAKPacket_pli_tag 5 +#define meshtastic_TAKPacket_chat_tag 6 +#define meshtastic_TAKPacket_detail_tag 7 + +/* Struct field encoding specification for nanopb */ +#define meshtastic_TAKPacket_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, is_compressed, 1) \ +X(a, STATIC, OPTIONAL, MESSAGE, contact, 2) \ +X(a, STATIC, OPTIONAL, MESSAGE, group, 3) \ +X(a, STATIC, OPTIONAL, MESSAGE, status, 4) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,pli,payload_variant.pli), 5) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,chat,payload_variant.chat), 6) \ +X(a, STATIC, ONEOF, BYTES, (payload_variant,detail,payload_variant.detail), 7) +#define meshtastic_TAKPacket_CALLBACK NULL +#define meshtastic_TAKPacket_DEFAULT NULL +#define meshtastic_TAKPacket_contact_MSGTYPE meshtastic_Contact +#define meshtastic_TAKPacket_group_MSGTYPE meshtastic_Group +#define meshtastic_TAKPacket_status_MSGTYPE meshtastic_Status +#define meshtastic_TAKPacket_payload_variant_pli_MSGTYPE meshtastic_PLI +#define meshtastic_TAKPacket_payload_variant_chat_MSGTYPE meshtastic_GeoChat + +#define meshtastic_GeoChat_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, STRING, message, 1) \ +X(a, STATIC, OPTIONAL, STRING, to, 2) \ +X(a, STATIC, OPTIONAL, STRING, to_callsign, 3) +#define meshtastic_GeoChat_CALLBACK NULL +#define meshtastic_GeoChat_DEFAULT NULL + +#define meshtastic_Group_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, role, 1) \ +X(a, STATIC, SINGULAR, UENUM, team, 2) +#define meshtastic_Group_CALLBACK NULL +#define meshtastic_Group_DEFAULT NULL + +#define meshtastic_Status_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, battery, 1) +#define meshtastic_Status_CALLBACK NULL +#define meshtastic_Status_DEFAULT NULL + +#define meshtastic_Contact_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, STRING, callsign, 1) \ +X(a, STATIC, SINGULAR, STRING, device_callsign, 2) +#define meshtastic_Contact_CALLBACK NULL +#define meshtastic_Contact_DEFAULT NULL + +#define meshtastic_PLI_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, SFIXED32, latitude_i, 1) \ +X(a, STATIC, SINGULAR, SFIXED32, longitude_i, 2) \ +X(a, STATIC, SINGULAR, INT32, altitude, 3) \ +X(a, STATIC, SINGULAR, UINT32, speed, 4) \ +X(a, STATIC, SINGULAR, UINT32, course, 5) +#define meshtastic_PLI_CALLBACK NULL +#define meshtastic_PLI_DEFAULT NULL + +extern const pb_msgdesc_t meshtastic_TAKPacket_msg; +extern const pb_msgdesc_t meshtastic_GeoChat_msg; +extern const pb_msgdesc_t meshtastic_Group_msg; +extern const pb_msgdesc_t meshtastic_Status_msg; +extern const pb_msgdesc_t meshtastic_Contact_msg; +extern const pb_msgdesc_t meshtastic_PLI_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define meshtastic_TAKPacket_fields &meshtastic_TAKPacket_msg +#define meshtastic_GeoChat_fields &meshtastic_GeoChat_msg +#define meshtastic_Group_fields &meshtastic_Group_msg +#define meshtastic_Status_fields &meshtastic_Status_msg +#define meshtastic_Contact_fields &meshtastic_Contact_msg +#define meshtastic_PLI_fields &meshtastic_PLI_msg + +/* Maximum encoded size of messages (where known) */ +#define MESHTASTIC_MESHTASTIC_ATAK_PB_H_MAX_SIZE meshtastic_TAKPacket_size +#define meshtastic_Contact_size 242 +#define meshtastic_GeoChat_size 444 +#define meshtastic_Group_size 4 +#define meshtastic_PLI_size 31 +#define meshtastic_Status_size 3 +#define meshtastic_TAKPacket_size 705 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/mesh/generated/meshtastic/cannedmessages.pb.cpp b/src/mesh/generated/meshtastic/cannedmessages.pb.cpp new file mode 100644 index 0000000..9f51e96 --- /dev/null +++ b/src/mesh/generated/meshtastic/cannedmessages.pb.cpp @@ -0,0 +1,12 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9 */ + +#include "meshtastic/cannedmessages.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(meshtastic_CannedMessageModuleConfig, meshtastic_CannedMessageModuleConfig, AUTO) + + + diff --git a/src/mesh/generated/meshtastic/cannedmessages.pb.h b/src/mesh/generated/meshtastic/cannedmessages.pb.h new file mode 100644 index 0000000..06d14b9 --- /dev/null +++ b/src/mesh/generated/meshtastic/cannedmessages.pb.h @@ -0,0 +1,50 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9 */ + +#ifndef PB_MESHTASTIC_MESHTASTIC_CANNEDMESSAGES_PB_H_INCLUDED +#define PB_MESHTASTIC_MESHTASTIC_CANNEDMESSAGES_PB_H_INCLUDED +#include + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Struct definitions */ +/* Canned message module configuration. */ +typedef struct _meshtastic_CannedMessageModuleConfig { + /* Predefined messages for canned message module separated by '|' characters. */ + char messages[201]; +} meshtastic_CannedMessageModuleConfig; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initializer values for message structs */ +#define meshtastic_CannedMessageModuleConfig_init_default {""} +#define meshtastic_CannedMessageModuleConfig_init_zero {""} + +/* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_CannedMessageModuleConfig_messages_tag 1 + +/* Struct field encoding specification for nanopb */ +#define meshtastic_CannedMessageModuleConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, STRING, messages, 1) +#define meshtastic_CannedMessageModuleConfig_CALLBACK NULL +#define meshtastic_CannedMessageModuleConfig_DEFAULT NULL + +extern const pb_msgdesc_t meshtastic_CannedMessageModuleConfig_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define meshtastic_CannedMessageModuleConfig_fields &meshtastic_CannedMessageModuleConfig_msg + +/* Maximum encoded size of messages (where known) */ +#define MESHTASTIC_MESHTASTIC_CANNEDMESSAGES_PB_H_MAX_SIZE meshtastic_CannedMessageModuleConfig_size +#define meshtastic_CannedMessageModuleConfig_size 203 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/mesh/generated/meshtastic/channel.pb.cpp b/src/mesh/generated/meshtastic/channel.pb.cpp new file mode 100644 index 0000000..52f923b --- /dev/null +++ b/src/mesh/generated/meshtastic/channel.pb.cpp @@ -0,0 +1,20 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9 */ + +#include "meshtastic/channel.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(meshtastic_ChannelSettings, meshtastic_ChannelSettings, AUTO) + + +PB_BIND(meshtastic_ModuleSettings, meshtastic_ModuleSettings, AUTO) + + +PB_BIND(meshtastic_Channel, meshtastic_Channel, AUTO) + + + + + diff --git a/src/mesh/generated/meshtastic/channel.pb.h b/src/mesh/generated/meshtastic/channel.pb.h new file mode 100644 index 0000000..3d617ae --- /dev/null +++ b/src/mesh/generated/meshtastic/channel.pb.h @@ -0,0 +1,198 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9 */ + +#ifndef PB_MESHTASTIC_MESHTASTIC_CHANNEL_PB_H_INCLUDED +#define PB_MESHTASTIC_MESHTASTIC_CHANNEL_PB_H_INCLUDED +#include + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Enum definitions */ +/* How this channel is being used (or not). + Note: this field is an enum to give us options for the future. + In particular, someday we might make a 'SCANNING' option. + SCANNING channels could have different frequencies and the radio would + occasionally check that freq to see if anything is being transmitted. + For devices that have multiple physical radios attached, we could keep multiple PRIMARY/SCANNING channels active at once to allow + cross band routing as needed. + If a device has only a single radio (the common case) only one channel can be PRIMARY at a time + (but any number of SECONDARY channels can't be sent received on that common frequency) */ +typedef enum _meshtastic_Channel_Role { + /* This channel is not in use right now */ + meshtastic_Channel_Role_DISABLED = 0, + /* This channel is used to set the frequency for the radio - all other enabled channels must be SECONDARY */ + meshtastic_Channel_Role_PRIMARY = 1, + /* Secondary channels are only used for encryption/decryption/authentication purposes. + Their radio settings (freq etc) are ignored, only psk is used. */ + meshtastic_Channel_Role_SECONDARY = 2 +} meshtastic_Channel_Role; + +/* Struct definitions */ +/* This message is specifically for modules to store per-channel configuration data. */ +typedef struct _meshtastic_ModuleSettings { + /* Bits of precision for the location sent in position packets. */ + uint32_t position_precision; + /* Controls whether or not the phone / clients should mute the current channel + Useful for noisy public channels you don't necessarily want to disable */ + bool is_client_muted; +} meshtastic_ModuleSettings; + +typedef PB_BYTES_ARRAY_T(32) meshtastic_ChannelSettings_psk_t; +/* This information can be encoded as a QRcode/url so that other users can configure + their radio to join the same channel. + A note about how channel names are shown to users: channelname-X + poundsymbol is a prefix used to indicate this is a channel name (idea from @professr). + Where X is a letter from A-Z (base 26) representing a hash of the PSK for this + channel - so that if the user changes anything about the channel (which does + force a new PSK) this letter will also change. Thus preventing user confusion if + two friends try to type in a channel name of "BobsChan" and then can't talk + because their PSKs will be different. + The PSK is hashed into this letter by "0x41 + [xor all bytes of the psk ] modulo 26" + This also allows the option of someday if people have the PSK off (zero), the + users COULD type in a channel name and be able to talk. + FIXME: Add description of multi-channel support and how primary vs secondary channels are used. + FIXME: explain how apps use channels for security. + explain how remote settings and remote gpio are managed as an example */ +typedef struct _meshtastic_ChannelSettings { + /* Deprecated in favor of LoraConfig.channel_num */ + uint32_t channel_num; + /* A simple pre-shared key for now for crypto. + Must be either 0 bytes (no crypto), 16 bytes (AES128), or 32 bytes (AES256). + A special shorthand is used for 1 byte long psks. + These psks should be treated as only minimally secure, + because they are listed in this source code. + Those bytes are mapped using the following scheme: + `0` = No crypto + `1` = The special "default" channel key: {0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59, 0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0x01} + `2` through 10 = The default channel key, except with 1 through 9 added to the last byte. + Shown to user as simple1 through 10 */ + meshtastic_ChannelSettings_psk_t psk; + /* A SHORT name that will be packed into the URL. + Less than 12 bytes. + Something for end users to call the channel + If this is the empty string it is assumed that this channel + is the special (minimally secure) "Default"channel. + In user interfaces it should be rendered as a local language translation of "X". + For channel_num hashing empty string will be treated as "X". + Where "X" is selected based on the English words listed above for ModemPreset */ + char name[12]; + /* Used to construct a globally unique channel ID. + The full globally unique ID will be: "name.id" where ID is shown as base36. + Assuming that the number of meshtastic users is below 20K (true for a long time) + the chance of this 64 bit random number colliding with anyone else is super low. + And the penalty for collision is low as well, it just means that anyone trying to decrypt channel messages might need to + try multiple candidate channels. + Any time a non wire compatible change is made to a channel, this field should be regenerated. + There are a small number of 'special' globally known (and fairly) insecure standard channels. + Those channels do not have a numeric id included in the settings, but instead it is pulled from + a table of well known IDs. + (see Well Known Channels FIXME) */ + uint32_t id; + /* If true, messages on the mesh will be sent to the *public* internet by any gateway ndoe */ + bool uplink_enabled; + /* If true, messages seen on the internet will be forwarded to the local mesh. */ + bool downlink_enabled; + /* Per-channel module settings. */ + bool has_module_settings; + meshtastic_ModuleSettings module_settings; +} meshtastic_ChannelSettings; + +/* A pair of a channel number, mode and the (sharable) settings for that channel */ +typedef struct _meshtastic_Channel { + /* The index of this channel in the channel table (from 0 to MAX_NUM_CHANNELS-1) + (Someday - not currently implemented) An index of -1 could be used to mean "set by name", + in which case the target node will find and set the channel by settings.name. */ + int8_t index; + /* The new settings, or NULL to disable that channel */ + bool has_settings; + meshtastic_ChannelSettings settings; + /* TODO: REPLACE */ + meshtastic_Channel_Role role; +} meshtastic_Channel; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Helper constants for enums */ +#define _meshtastic_Channel_Role_MIN meshtastic_Channel_Role_DISABLED +#define _meshtastic_Channel_Role_MAX meshtastic_Channel_Role_SECONDARY +#define _meshtastic_Channel_Role_ARRAYSIZE ((meshtastic_Channel_Role)(meshtastic_Channel_Role_SECONDARY+1)) + + + +#define meshtastic_Channel_role_ENUMTYPE meshtastic_Channel_Role + + +/* Initializer values for message structs */ +#define meshtastic_ChannelSettings_init_default {0, {0, {0}}, "", 0, 0, 0, false, meshtastic_ModuleSettings_init_default} +#define meshtastic_ModuleSettings_init_default {0, 0} +#define meshtastic_Channel_init_default {0, false, meshtastic_ChannelSettings_init_default, _meshtastic_Channel_Role_MIN} +#define meshtastic_ChannelSettings_init_zero {0, {0, {0}}, "", 0, 0, 0, false, meshtastic_ModuleSettings_init_zero} +#define meshtastic_ModuleSettings_init_zero {0, 0} +#define meshtastic_Channel_init_zero {0, false, meshtastic_ChannelSettings_init_zero, _meshtastic_Channel_Role_MIN} + +/* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_ModuleSettings_position_precision_tag 1 +#define meshtastic_ModuleSettings_is_client_muted_tag 2 +#define meshtastic_ChannelSettings_channel_num_tag 1 +#define meshtastic_ChannelSettings_psk_tag 2 +#define meshtastic_ChannelSettings_name_tag 3 +#define meshtastic_ChannelSettings_id_tag 4 +#define meshtastic_ChannelSettings_uplink_enabled_tag 5 +#define meshtastic_ChannelSettings_downlink_enabled_tag 6 +#define meshtastic_ChannelSettings_module_settings_tag 7 +#define meshtastic_Channel_index_tag 1 +#define meshtastic_Channel_settings_tag 2 +#define meshtastic_Channel_role_tag 3 + +/* Struct field encoding specification for nanopb */ +#define meshtastic_ChannelSettings_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, channel_num, 1) \ +X(a, STATIC, SINGULAR, BYTES, psk, 2) \ +X(a, STATIC, SINGULAR, STRING, name, 3) \ +X(a, STATIC, SINGULAR, FIXED32, id, 4) \ +X(a, STATIC, SINGULAR, BOOL, uplink_enabled, 5) \ +X(a, STATIC, SINGULAR, BOOL, downlink_enabled, 6) \ +X(a, STATIC, OPTIONAL, MESSAGE, module_settings, 7) +#define meshtastic_ChannelSettings_CALLBACK NULL +#define meshtastic_ChannelSettings_DEFAULT NULL +#define meshtastic_ChannelSettings_module_settings_MSGTYPE meshtastic_ModuleSettings + +#define meshtastic_ModuleSettings_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, position_precision, 1) \ +X(a, STATIC, SINGULAR, BOOL, is_client_muted, 2) +#define meshtastic_ModuleSettings_CALLBACK NULL +#define meshtastic_ModuleSettings_DEFAULT NULL + +#define meshtastic_Channel_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, INT32, index, 1) \ +X(a, STATIC, OPTIONAL, MESSAGE, settings, 2) \ +X(a, STATIC, SINGULAR, UENUM, role, 3) +#define meshtastic_Channel_CALLBACK NULL +#define meshtastic_Channel_DEFAULT NULL +#define meshtastic_Channel_settings_MSGTYPE meshtastic_ChannelSettings + +extern const pb_msgdesc_t meshtastic_ChannelSettings_msg; +extern const pb_msgdesc_t meshtastic_ModuleSettings_msg; +extern const pb_msgdesc_t meshtastic_Channel_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define meshtastic_ChannelSettings_fields &meshtastic_ChannelSettings_msg +#define meshtastic_ModuleSettings_fields &meshtastic_ModuleSettings_msg +#define meshtastic_Channel_fields &meshtastic_Channel_msg + +/* Maximum encoded size of messages (where known) */ +#define MESHTASTIC_MESHTASTIC_CHANNEL_PB_H_MAX_SIZE meshtastic_Channel_size +#define meshtastic_ChannelSettings_size 72 +#define meshtastic_Channel_size 87 +#define meshtastic_ModuleSettings_size 8 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/mesh/generated/meshtastic/clientonly.pb.cpp b/src/mesh/generated/meshtastic/clientonly.pb.cpp new file mode 100644 index 0000000..d99af8c --- /dev/null +++ b/src/mesh/generated/meshtastic/clientonly.pb.cpp @@ -0,0 +1,12 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9 */ + +#include "meshtastic/clientonly.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(meshtastic_DeviceProfile, meshtastic_DeviceProfile, 2) + + + diff --git a/src/mesh/generated/meshtastic/clientonly.pb.h b/src/mesh/generated/meshtastic/clientonly.pb.h new file mode 100644 index 0000000..bf32d78 --- /dev/null +++ b/src/mesh/generated/meshtastic/clientonly.pb.h @@ -0,0 +1,90 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9 */ + +#ifndef PB_MESHTASTIC_MESHTASTIC_CLIENTONLY_PB_H_INCLUDED +#define PB_MESHTASTIC_MESHTASTIC_CLIENTONLY_PB_H_INCLUDED +#include +#include "meshtastic/localonly.pb.h" +#include "meshtastic/mesh.pb.h" + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Struct definitions */ +/* This abstraction is used to contain any configuration for provisioning a node on any client. + It is useful for importing and exporting configurations. */ +typedef struct _meshtastic_DeviceProfile { + /* Long name for the node */ + bool has_long_name; + char long_name[40]; + /* Short name of the node */ + bool has_short_name; + char short_name[5]; + /* The url of the channels from our node */ + pb_callback_t channel_url; + /* The Config of the node */ + bool has_config; + meshtastic_LocalConfig config; + /* The ModuleConfig of the node */ + bool has_module_config; + meshtastic_LocalModuleConfig module_config; + /* Fixed position data */ + bool has_fixed_position; + meshtastic_Position fixed_position; + /* Ringtone for ExternalNotification */ + bool has_ringtone; + char ringtone[231]; + /* Predefined messages for CannedMessage */ + bool has_canned_messages; + char canned_messages[201]; +} meshtastic_DeviceProfile; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initializer values for message structs */ +#define meshtastic_DeviceProfile_init_default {false, "", false, "", {{NULL}, NULL}, false, meshtastic_LocalConfig_init_default, false, meshtastic_LocalModuleConfig_init_default, false, meshtastic_Position_init_default, false, "", false, ""} +#define meshtastic_DeviceProfile_init_zero {false, "", false, "", {{NULL}, NULL}, false, meshtastic_LocalConfig_init_zero, false, meshtastic_LocalModuleConfig_init_zero, false, meshtastic_Position_init_zero, false, "", false, ""} + +/* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_DeviceProfile_long_name_tag 1 +#define meshtastic_DeviceProfile_short_name_tag 2 +#define meshtastic_DeviceProfile_channel_url_tag 3 +#define meshtastic_DeviceProfile_config_tag 4 +#define meshtastic_DeviceProfile_module_config_tag 5 +#define meshtastic_DeviceProfile_fixed_position_tag 6 +#define meshtastic_DeviceProfile_ringtone_tag 7 +#define meshtastic_DeviceProfile_canned_messages_tag 8 + +/* Struct field encoding specification for nanopb */ +#define meshtastic_DeviceProfile_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, STRING, long_name, 1) \ +X(a, STATIC, OPTIONAL, STRING, short_name, 2) \ +X(a, CALLBACK, OPTIONAL, STRING, channel_url, 3) \ +X(a, STATIC, OPTIONAL, MESSAGE, config, 4) \ +X(a, STATIC, OPTIONAL, MESSAGE, module_config, 5) \ +X(a, STATIC, OPTIONAL, MESSAGE, fixed_position, 6) \ +X(a, STATIC, OPTIONAL, STRING, ringtone, 7) \ +X(a, STATIC, OPTIONAL, STRING, canned_messages, 8) +#define meshtastic_DeviceProfile_CALLBACK pb_default_field_callback +#define meshtastic_DeviceProfile_DEFAULT NULL +#define meshtastic_DeviceProfile_config_MSGTYPE meshtastic_LocalConfig +#define meshtastic_DeviceProfile_module_config_MSGTYPE meshtastic_LocalModuleConfig +#define meshtastic_DeviceProfile_fixed_position_MSGTYPE meshtastic_Position + +extern const pb_msgdesc_t meshtastic_DeviceProfile_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define meshtastic_DeviceProfile_fields &meshtastic_DeviceProfile_msg + +/* Maximum encoded size of messages (where known) */ +/* meshtastic_DeviceProfile_size depends on runtime parameters */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/mesh/generated/meshtastic/config.pb.cpp b/src/mesh/generated/meshtastic/config.pb.cpp new file mode 100644 index 0000000..23f4d54 --- /dev/null +++ b/src/mesh/generated/meshtastic/config.pb.cpp @@ -0,0 +1,68 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9 */ + +#include "meshtastic/config.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(meshtastic_Config, meshtastic_Config, AUTO) + + +PB_BIND(meshtastic_Config_DeviceConfig, meshtastic_Config_DeviceConfig, AUTO) + + +PB_BIND(meshtastic_Config_PositionConfig, meshtastic_Config_PositionConfig, AUTO) + + +PB_BIND(meshtastic_Config_PowerConfig, meshtastic_Config_PowerConfig, AUTO) + + +PB_BIND(meshtastic_Config_NetworkConfig, meshtastic_Config_NetworkConfig, AUTO) + + +PB_BIND(meshtastic_Config_NetworkConfig_IpV4Config, meshtastic_Config_NetworkConfig_IpV4Config, AUTO) + + +PB_BIND(meshtastic_Config_DisplayConfig, meshtastic_Config_DisplayConfig, AUTO) + + +PB_BIND(meshtastic_Config_LoRaConfig, meshtastic_Config_LoRaConfig, 2) + + +PB_BIND(meshtastic_Config_BluetoothConfig, meshtastic_Config_BluetoothConfig, AUTO) + + +PB_BIND(meshtastic_Config_SecurityConfig, meshtastic_Config_SecurityConfig, AUTO) + + +PB_BIND(meshtastic_Config_SessionkeyConfig, meshtastic_Config_SessionkeyConfig, AUTO) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h new file mode 100644 index 0000000..fab23ae --- /dev/null +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -0,0 +1,980 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9 */ + +#ifndef PB_MESHTASTIC_MESHTASTIC_CONFIG_PB_H_INCLUDED +#define PB_MESHTASTIC_MESHTASTIC_CONFIG_PB_H_INCLUDED +#include +#include "meshtastic/device_ui.pb.h" + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Enum definitions */ +/* Defines the device's role on the Mesh network */ +typedef enum _meshtastic_Config_DeviceConfig_Role { + /* Description: App connected or stand alone messaging device. + Technical Details: Default Role */ + meshtastic_Config_DeviceConfig_Role_CLIENT = 0, + /* Description: Device that does not forward packets from other devices. */ + meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE = 1, + /* Description: Infrastructure node for extending network coverage by relaying messages. Visible in Nodes list. + Technical Details: Mesh packets will prefer to be routed over this node. This node will not be used by client apps. + The wifi radio and the oled screen will be put to sleep. + This mode may still potentially have higher power usage due to it's preference in message rebroadcasting on the mesh. */ + meshtastic_Config_DeviceConfig_Role_ROUTER = 2, + meshtastic_Config_DeviceConfig_Role_ROUTER_CLIENT = 3, + /* Description: Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list. + Technical Details: Mesh packets will simply be rebroadcasted over this node. Nodes configured with this role will not originate NodeInfo, Position, Telemetry + or any other packet type. They will simply rebroadcast any mesh packets on the same frequency, channel num, spread factor, and coding rate. */ + meshtastic_Config_DeviceConfig_Role_REPEATER = 4, + /* Description: Broadcasts GPS position packets as priority. + Technical Details: Position Mesh packets will be prioritized higher and sent more frequently by default. + When used in conjunction with power.is_power_saving = true, nodes will wake up, + send position, and then sleep for position.position_broadcast_secs seconds. */ + meshtastic_Config_DeviceConfig_Role_TRACKER = 5, + /* Description: Broadcasts telemetry packets as priority. + Technical Details: Telemetry Mesh packets will be prioritized higher and sent more frequently by default. + When used in conjunction with power.is_power_saving = true, nodes will wake up, + send environment telemetry, and then sleep for telemetry.environment_update_interval seconds. */ + meshtastic_Config_DeviceConfig_Role_SENSOR = 6, + /* Description: Optimized for ATAK system communication and reduces routine broadcasts. + Technical Details: Used for nodes dedicated for connection to an ATAK EUD. + Turns off many of the routine broadcasts to favor CoT packet stream + from the Meshtastic ATAK plugin -> IMeshService -> Node */ + meshtastic_Config_DeviceConfig_Role_TAK = 7, + /* Description: Device that only broadcasts as needed for stealth or power savings. + Technical Details: Used for nodes that "only speak when spoken to" + Turns all of the routine broadcasts but allows for ad-hoc communication + Still rebroadcasts, but with local only rebroadcast mode (known meshes only) + Can be used for clandestine operation or to dramatically reduce airtime / power consumption */ + meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN = 8, + /* Description: Broadcasts location as message to default channel regularly for to assist with device recovery. + Technical Details: Used to automatically send a text message to the mesh + with the current position of the device on a frequent interval: + "I'm lost! Position: lat / long" */ + meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND = 9, + /* Description: Enables automatic TAK PLI broadcasts and reduces routine broadcasts. + Technical Details: Turns off many of the routine broadcasts to favor ATAK CoT packet stream + and automatic TAK PLI (position location information) broadcasts. + Uses position module configuration to determine TAK PLI broadcast interval. */ + meshtastic_Config_DeviceConfig_Role_TAK_TRACKER = 10 +} meshtastic_Config_DeviceConfig_Role; + +/* Defines the device's behavior for how messages are rebroadcast */ +typedef enum _meshtastic_Config_DeviceConfig_RebroadcastMode { + /* Default behavior. + Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora params. */ + meshtastic_Config_DeviceConfig_RebroadcastMode_ALL = 0, + /* Same as behavior as ALL but skips packet decoding and simply rebroadcasts them. + Only available in Repeater role. Setting this on any other roles will result in ALL behavior. */ + meshtastic_Config_DeviceConfig_RebroadcastMode_ALL_SKIP_DECODING = 1, + /* Ignores observed messages from foreign meshes that are open or those which it cannot decrypt. + Only rebroadcasts message on the nodes local primary / secondary channels. */ + meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY = 2, + /* Ignores observed messages from foreign meshes like LOCAL_ONLY, + but takes it step further by also ignoring messages from nodenums not in the node's known list (NodeDB) */ + meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY = 3, + /* Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role. */ + meshtastic_Config_DeviceConfig_RebroadcastMode_NONE = 4, + /* Ignores packets from non-standard portnums such as: TAK, RangeTest, PaxCounter, etc. + Only rebroadcasts packets with standard portnums: NodeInfo, Text, Position, Telemetry, and Routing. */ + meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY = 5 +} meshtastic_Config_DeviceConfig_RebroadcastMode; + +/* Bit field of boolean configuration options, indicating which optional + fields to include when assembling POSITION messages. + Longitude, latitude, altitude, speed, heading, and DOP + are always included (also time if GPS-synced) + NOTE: the more fields are included, the larger the message will be - + leading to longer airtime and a higher risk of packet loss */ +typedef enum _meshtastic_Config_PositionConfig_PositionFlags { + /* Required for compilation */ + meshtastic_Config_PositionConfig_PositionFlags_UNSET = 0, + /* Include an altitude value (if available) */ + meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE = 1, + /* Altitude value is MSL */ + meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE_MSL = 2, + /* Include geoidal separation */ + meshtastic_Config_PositionConfig_PositionFlags_GEOIDAL_SEPARATION = 4, + /* Include the DOP value ; PDOP used by default, see below */ + meshtastic_Config_PositionConfig_PositionFlags_DOP = 8, + /* If POS_DOP set, send separate HDOP / VDOP values instead of PDOP */ + meshtastic_Config_PositionConfig_PositionFlags_HVDOP = 16, + /* Include number of "satellites in view" */ + meshtastic_Config_PositionConfig_PositionFlags_SATINVIEW = 32, + /* Include a sequence number incremented per packet */ + meshtastic_Config_PositionConfig_PositionFlags_SEQ_NO = 64, + /* Include positional timestamp (from GPS solution) */ + meshtastic_Config_PositionConfig_PositionFlags_TIMESTAMP = 128, + /* Include positional heading + Intended for use with vehicle not walking speeds + walking speeds are likely to be error prone like the compass */ + meshtastic_Config_PositionConfig_PositionFlags_HEADING = 256, + /* Include positional speed + Intended for use with vehicle not walking speeds + walking speeds are likely to be error prone like the compass */ + meshtastic_Config_PositionConfig_PositionFlags_SPEED = 512 +} meshtastic_Config_PositionConfig_PositionFlags; + +typedef enum _meshtastic_Config_PositionConfig_GpsMode { + /* GPS is present but disabled */ + meshtastic_Config_PositionConfig_GpsMode_DISABLED = 0, + /* GPS is present and enabled */ + meshtastic_Config_PositionConfig_GpsMode_ENABLED = 1, + /* GPS is not present on the device */ + meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT = 2 +} meshtastic_Config_PositionConfig_GpsMode; + +typedef enum _meshtastic_Config_NetworkConfig_AddressMode { + /* obtain ip address via DHCP */ + meshtastic_Config_NetworkConfig_AddressMode_DHCP = 0, + /* use static ip address */ + meshtastic_Config_NetworkConfig_AddressMode_STATIC = 1 +} meshtastic_Config_NetworkConfig_AddressMode; + +/* How the GPS coordinates are displayed on the OLED screen. */ +typedef enum _meshtastic_Config_DisplayConfig_GpsCoordinateFormat { + /* GPS coordinates are displayed in the normal decimal degrees format: + DD.DDDDDD DDD.DDDDDD */ + meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DEC = 0, + /* GPS coordinates are displayed in the degrees minutes seconds format: + DD°MM'SS"C DDD°MM'SS"C, where C is the compass point representing the locations quadrant */ + meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS = 1, + /* Universal Transverse Mercator format: + ZZB EEEEEE NNNNNNN, where Z is zone, B is band, E is easting, N is northing */ + meshtastic_Config_DisplayConfig_GpsCoordinateFormat_UTM = 2, + /* Military Grid Reference System format: + ZZB CD EEEEE NNNNN, where Z is zone, B is band, C is the east 100k square, D is the north 100k square, + E is easting, N is northing */ + meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MGRS = 3, + /* Open Location Code (aka Plus Codes). */ + meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OLC = 4, + /* Ordnance Survey Grid Reference (the National Grid System of the UK). + Format: AB EEEEE NNNNN, where A is the east 100k square, B is the north 100k square, + E is the easting, N is the northing */ + meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR = 5 +} meshtastic_Config_DisplayConfig_GpsCoordinateFormat; + +/* Unit display preference */ +typedef enum _meshtastic_Config_DisplayConfig_DisplayUnits { + /* Metric (Default) */ + meshtastic_Config_DisplayConfig_DisplayUnits_METRIC = 0, + /* Imperial */ + meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL = 1 +} meshtastic_Config_DisplayConfig_DisplayUnits; + +/* Override OLED outo detect with this if it fails. */ +typedef enum _meshtastic_Config_DisplayConfig_OledType { + /* Default / Auto */ + meshtastic_Config_DisplayConfig_OledType_OLED_AUTO = 0, + /* Default / Auto */ + meshtastic_Config_DisplayConfig_OledType_OLED_SSD1306 = 1, + /* Default / Auto */ + meshtastic_Config_DisplayConfig_OledType_OLED_SH1106 = 2, + /* Can not be auto detected but set by proto. Used for 128x128 screens */ + meshtastic_Config_DisplayConfig_OledType_OLED_SH1107 = 3 +} meshtastic_Config_DisplayConfig_OledType; + +typedef enum _meshtastic_Config_DisplayConfig_DisplayMode { + /* Default. The old style for the 128x64 OLED screen */ + meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT = 0, + /* Rearrange display elements to cater for bicolor OLED displays */ + meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR = 1, + /* Same as TwoColor, but with inverted top bar. Not so good for Epaper displays */ + meshtastic_Config_DisplayConfig_DisplayMode_INVERTED = 2, + /* TFT Full Color Displays (not implemented yet) */ + meshtastic_Config_DisplayConfig_DisplayMode_COLOR = 3 +} meshtastic_Config_DisplayConfig_DisplayMode; + +typedef enum _meshtastic_Config_DisplayConfig_CompassOrientation { + /* The compass and the display are in the same orientation. */ + meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0 = 0, + /* Rotate the compass by 90 degrees. */ + meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90 = 1, + /* Rotate the compass by 180 degrees. */ + meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180 = 2, + /* Rotate the compass by 270 degrees. */ + meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270 = 3, + /* Don't rotate the compass, but invert the result. */ + meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED = 4, + /* Rotate the compass by 90 degrees and invert. */ + meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED = 5, + /* Rotate the compass by 180 degrees and invert. */ + meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED = 6, + /* Rotate the compass by 270 degrees and invert. */ + meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED = 7 +} meshtastic_Config_DisplayConfig_CompassOrientation; + +typedef enum _meshtastic_Config_LoRaConfig_RegionCode { + /* Region is not set */ + meshtastic_Config_LoRaConfig_RegionCode_UNSET = 0, + /* United States */ + meshtastic_Config_LoRaConfig_RegionCode_US = 1, + /* European Union 433mhz */ + meshtastic_Config_LoRaConfig_RegionCode_EU_433 = 2, + /* European Union 868mhz */ + meshtastic_Config_LoRaConfig_RegionCode_EU_868 = 3, + /* China */ + meshtastic_Config_LoRaConfig_RegionCode_CN = 4, + /* Japan */ + meshtastic_Config_LoRaConfig_RegionCode_JP = 5, + /* Australia / New Zealand */ + meshtastic_Config_LoRaConfig_RegionCode_ANZ = 6, + /* Korea */ + meshtastic_Config_LoRaConfig_RegionCode_KR = 7, + /* Taiwan */ + meshtastic_Config_LoRaConfig_RegionCode_TW = 8, + /* Russia */ + meshtastic_Config_LoRaConfig_RegionCode_RU = 9, + /* India */ + meshtastic_Config_LoRaConfig_RegionCode_IN = 10, + /* New Zealand 865mhz */ + meshtastic_Config_LoRaConfig_RegionCode_NZ_865 = 11, + /* Thailand */ + meshtastic_Config_LoRaConfig_RegionCode_TH = 12, + /* WLAN Band */ + meshtastic_Config_LoRaConfig_RegionCode_LORA_24 = 13, + /* Ukraine 433mhz */ + meshtastic_Config_LoRaConfig_RegionCode_UA_433 = 14, + /* Ukraine 868mhz */ + meshtastic_Config_LoRaConfig_RegionCode_UA_868 = 15, + /* Malaysia 433mhz */ + meshtastic_Config_LoRaConfig_RegionCode_MY_433 = 16, + /* Malaysia 919mhz */ + meshtastic_Config_LoRaConfig_RegionCode_MY_919 = 17, + /* Singapore 923mhz */ + meshtastic_Config_LoRaConfig_RegionCode_SG_923 = 18, + /* Philippines 433mhz */ + meshtastic_Config_LoRaConfig_RegionCode_PH_433 = 19, + /* Philippines 868mhz */ + meshtastic_Config_LoRaConfig_RegionCode_PH_868 = 20, + /* Philippines 915mhz */ + meshtastic_Config_LoRaConfig_RegionCode_PH_915 = 21 +} meshtastic_Config_LoRaConfig_RegionCode; + +/* Standard predefined channel settings + Note: these mappings must match ModemPreset Choice in the device code. */ +typedef enum _meshtastic_Config_LoRaConfig_ModemPreset { + /* Long Range - Fast */ + meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST = 0, + /* Long Range - Slow */ + meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW = 1, + /* Very Long Range - Slow + Deprecated in 2.5: Works only with txco and is unusably slow */ + meshtastic_Config_LoRaConfig_ModemPreset_VERY_LONG_SLOW = 2, + /* Medium Range - Slow */ + meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW = 3, + /* Medium Range - Fast */ + meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST = 4, + /* Short Range - Slow */ + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW = 5, + /* Short Range - Fast */ + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST = 6, + /* Long Range - Moderately Fast */ + meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE = 7, + /* Short Range - Turbo + This is the fastest preset and the only one with 500kHz bandwidth. + It is not legal to use in all regions due to this wider bandwidth. */ + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO = 8 +} meshtastic_Config_LoRaConfig_ModemPreset; + +typedef enum _meshtastic_Config_BluetoothConfig_PairingMode { + /* Device generates a random PIN that will be shown on the screen of the device for pairing */ + meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN = 0, + /* Device requires a specified fixed PIN for pairing */ + meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN = 1, + /* Device requires no PIN for pairing */ + meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN = 2 +} meshtastic_Config_BluetoothConfig_PairingMode; + +/* Struct definitions */ +/* Configuration */ +typedef struct _meshtastic_Config_DeviceConfig { + /* Sets the role of node */ + meshtastic_Config_DeviceConfig_Role role; + /* Disabling this will disable the SerialConsole by not initilizing the StreamAPI + Moved to SecurityConfig */ + bool serial_enabled; + /* For boards without a hard wired button, this is the pin number that will be used + Boards that have more than one button can swap the function with this one. defaults to BUTTON_PIN if defined. */ + uint32_t button_gpio; + /* For boards without a PWM buzzer, this is the pin number that will be used + Defaults to PIN_BUZZER if defined. */ + uint32_t buzzer_gpio; + /* Sets the role of node */ + meshtastic_Config_DeviceConfig_RebroadcastMode rebroadcast_mode; + /* Send our nodeinfo this often + Defaults to 900 Seconds (15 minutes) */ + uint32_t node_info_broadcast_secs; + /* Treat double tap interrupt on supported accelerometers as a button press if set to true */ + bool double_tap_as_button_press; + /* If true, device is considered to be "managed" by a mesh administrator + Clients should then limit available configuration and administrative options inside the user interface + Moved to SecurityConfig */ + bool is_managed; + /* Disables the triple-press of user button to enable or disable GPS */ + bool disable_triple_click; + /* POSIX Timezone definition string from https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv. */ + char tzdef[65]; + /* If true, disable the default blinking LED (LED_PIN) behavior on the device */ + bool led_heartbeat_disabled; +} meshtastic_Config_DeviceConfig; + +/* Position Config */ +typedef struct _meshtastic_Config_PositionConfig { + /* We should send our position this often (but only if it has changed significantly) + Defaults to 15 minutes */ + uint32_t position_broadcast_secs; + /* Adaptive position braoadcast, which is now the default. */ + bool position_broadcast_smart_enabled; + /* If set, this node is at a fixed position. + We will generate GPS position updates at the regular interval, but use whatever the last lat/lon/alt we have for the node. + The lat/lon/alt can be set by an internal GPS or with the help of the app. */ + bool fixed_position; + /* Is GPS enabled for this node? */ + bool gps_enabled; + /* How often should we try to get GPS position (in seconds) + or zero for the default of once every 30 seconds + or a very large value (maxint) to update only once at boot. */ + uint32_t gps_update_interval; + /* Deprecated in favor of using smart / regular broadcast intervals as implicit attempt time */ + uint32_t gps_attempt_time; + /* Bit field of boolean configuration options for POSITION messages + (bitwise OR of PositionFlags) */ + uint32_t position_flags; + /* (Re)define GPS_RX_PIN for your board. */ + uint32_t rx_gpio; + /* (Re)define GPS_TX_PIN for your board. */ + uint32_t tx_gpio; + /* The minimum distance in meters traveled (since the last send) before we can send a position to the mesh if position_broadcast_smart_enabled */ + uint32_t broadcast_smart_minimum_distance; + /* The minimum number of seconds (since the last send) before we can send a position to the mesh if position_broadcast_smart_enabled */ + uint32_t broadcast_smart_minimum_interval_secs; + /* (Re)define PIN_GPS_EN for your board. */ + uint32_t gps_en_gpio; + /* Set where GPS is enabled, disabled, or not present */ + meshtastic_Config_PositionConfig_GpsMode gps_mode; +} meshtastic_Config_PositionConfig; + +/* Power Config\ + See [Power Config](/docs/settings/config/power) for additional power config details. */ +typedef struct _meshtastic_Config_PowerConfig { + /* Description: Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio. + Don't use this setting if you want to use your device with the phone apps or are using a device without a user button. + Technical Details: Works for ESP32 devices and NRF52 devices in the Sensor or Tracker roles */ + bool is_power_saving; + /* Description: If non-zero, the device will fully power off this many seconds after external power is removed. */ + uint32_t on_battery_shutdown_after_secs; + /* Ratio of voltage divider for battery pin eg. 3.20 (R1=100k, R2=220k) + Overrides the ADC_MULTIPLIER defined in variant for battery voltage calculation. + https://meshtastic.org/docs/configuration/radio/power/#adc-multiplier-override + Should be set to floating point value between 2 and 6 */ + float adc_multiplier_override; + /* Description: The number of seconds for to wait before turning off BLE in No Bluetooth states + Technical Details: ESP32 Only 0 for default of 1 minute */ + uint32_t wait_bluetooth_secs; + /* Super Deep Sleep Seconds + While in Light Sleep if mesh_sds_timeout_secs is exceeded we will lower into super deep sleep + for this value (default 1 year) or a button press + 0 for default of one year */ + uint32_t sds_secs; + /* Description: In light sleep the CPU is suspended, LoRa radio is on, BLE is off an GPS is on + Technical Details: ESP32 Only 0 for default of 300 */ + uint32_t ls_secs; + /* Description: While in light sleep when we receive packets on the LoRa radio we will wake and handle them and stay awake in no BLE mode for this value + Technical Details: ESP32 Only 0 for default of 10 seconds */ + uint32_t min_wake_secs; + /* I2C address of INA_2XX to use for reading device battery voltage */ + uint8_t device_battery_ina_address; + /* If non-zero, we want powermon log outputs. With the particular (bitfield) sources enabled. + Note: we picked an ID of 32 so that lower more efficient IDs can be used for more frequently used options. */ + uint64_t powermon_enables; +} meshtastic_Config_PowerConfig; + +typedef struct _meshtastic_Config_NetworkConfig_IpV4Config { + /* Static IP address */ + uint32_t ip; + /* Static gateway address */ + uint32_t gateway; + /* Static subnet mask */ + uint32_t subnet; + /* Static DNS server address */ + uint32_t dns; +} meshtastic_Config_NetworkConfig_IpV4Config; + +/* Network Config */ +typedef struct _meshtastic_Config_NetworkConfig { + /* Enable WiFi (disables Bluetooth) */ + bool wifi_enabled; + /* If set, this node will try to join the specified wifi network and + acquire an address via DHCP */ + char wifi_ssid[33]; + /* If set, will be use to authenticate to the named wifi */ + char wifi_psk[65]; + /* NTP server to use if WiFi is conneced, defaults to `0.pool.ntp.org` */ + char ntp_server[33]; + /* Enable Ethernet */ + bool eth_enabled; + /* acquire an address via DHCP or assign static */ + meshtastic_Config_NetworkConfig_AddressMode address_mode; + /* struct to keep static address */ + bool has_ipv4_config; + meshtastic_Config_NetworkConfig_IpV4Config ipv4_config; + /* rsyslog Server and Port */ + char rsyslog_server[33]; +} meshtastic_Config_NetworkConfig; + +/* Display Config */ +typedef struct _meshtastic_Config_DisplayConfig { + /* Number of seconds the screen stays on after pressing the user button or receiving a message + 0 for default of one minute MAXUINT for always on */ + uint32_t screen_on_secs; + /* How the GPS coordinates are formatted on the OLED screen. */ + meshtastic_Config_DisplayConfig_GpsCoordinateFormat gps_format; + /* Automatically toggles to the next page on the screen like a carousel, based the specified interval in seconds. + Potentially useful for devices without user buttons. */ + uint32_t auto_screen_carousel_secs; + /* If this is set, the displayed compass will always point north. if unset, the old behaviour + (top of display is heading direction) is used. */ + bool compass_north_top; + /* Flip screen vertically, for cases that mount the screen upside down */ + bool flip_screen; + /* Perferred display units */ + meshtastic_Config_DisplayConfig_DisplayUnits units; + /* Override auto-detect in screen */ + meshtastic_Config_DisplayConfig_OledType oled; + /* Display Mode */ + meshtastic_Config_DisplayConfig_DisplayMode displaymode; + /* Print first line in pseudo-bold? FALSE is original style, TRUE is bold */ + bool heading_bold; + /* Should we wake the screen up on accelerometer detected motion or tap */ + bool wake_on_tap_or_motion; + /* Indicates how to rotate or invert the compass output to accurate display on the display. */ + meshtastic_Config_DisplayConfig_CompassOrientation compass_orientation; +} meshtastic_Config_DisplayConfig; + +/* Lora Config */ +typedef struct _meshtastic_Config_LoRaConfig { + /* When enabled, the `modem_preset` fields will be adhered to, else the `bandwidth`/`spread_factor`/`coding_rate` + will be taked from their respective manually defined fields */ + bool use_preset; + /* Either modem_config or bandwidth/spreading/coding will be specified - NOT BOTH. + As a heuristic: If bandwidth is specified, do not use modem_config. + Because protobufs take ZERO space when the value is zero this works out nicely. + This value is replaced by bandwidth/spread_factor/coding_rate. + If you'd like to experiment with other options add them to MeshRadio.cpp in the device code. */ + meshtastic_Config_LoRaConfig_ModemPreset modem_preset; + /* Bandwidth in MHz + Certain bandwidth numbers are 'special' and will be converted to the + appropriate floating point value: 31 -> 31.25MHz */ + uint16_t bandwidth; + /* A number from 7 to 12. + Indicates number of chirps per symbol as 1< 7 results in the default */ + uint32_t hop_limit; + /* Disable TX from the LoRa radio. Useful for hot-swapping antennas and other tests. + Defaults to false */ + bool tx_enabled; + /* If zero, then use default max legal continuous power (ie. something that won't + burn out the radio hardware) + In most cases you should use zero here. + Units are in dBm. */ + int8_t tx_power; + /* This controls the actual hardware frequency the radio transmits on. + Most users should never need to be exposed to this field/concept. + A channel number between 1 and NUM_CHANNELS (whatever the max is in the current region). + If ZERO then the rule is "use the old channel name hash based + algorithm to derive the channel number") + If using the hash algorithm the channel number will be: hash(channel_name) % + NUM_CHANNELS (Where num channels depends on the regulatory region). */ + uint16_t channel_num; + /* If true, duty cycle limits will be exceeded and thus you're possibly not following + the local regulations if you're not a HAM. + Has no effect if the duty cycle of the used region is 100%. */ + bool override_duty_cycle; + /* If true, sets RX boosted gain mode on SX126X based radios */ + bool sx126x_rx_boosted_gain; + /* This parameter is for advanced users and licensed HAM radio operators. + Ignore Channel Calculation and use this frequency instead. The frequency_offset + will still be applied. This will allow you to use out-of-band frequencies. + Please respect your local laws and regulations. If you are a HAM, make sure you + enable HAM mode and turn off encryption. */ + float override_frequency; + /* If true, disable the build-in PA FAN using pin define in RF95_FAN_EN. */ + bool pa_fan_disabled; + /* For testing it is useful sometimes to force a node to never listen to + particular other nodes (simulating radio out of range). All nodenums listed + in ignore_incoming will have packets they send dropped on receive (by router.cpp) */ + pb_size_t ignore_incoming_count; + uint32_t ignore_incoming[3]; + /* If true, the device will not process any packets received via LoRa that passed via MQTT anywhere on the path towards it. */ + bool ignore_mqtt; + /* Sets the ok_to_mqtt bit on outgoing packets */ + bool config_ok_to_mqtt; +} meshtastic_Config_LoRaConfig; + +typedef struct _meshtastic_Config_BluetoothConfig { + /* Enable Bluetooth on the device */ + bool enabled; + /* Determines the pairing strategy for the device */ + meshtastic_Config_BluetoothConfig_PairingMode mode; + /* Specified PIN for PairingMode.FixedPin */ + uint32_t fixed_pin; +} meshtastic_Config_BluetoothConfig; + +typedef PB_BYTES_ARRAY_T(32) meshtastic_Config_SecurityConfig_public_key_t; +typedef PB_BYTES_ARRAY_T(32) meshtastic_Config_SecurityConfig_private_key_t; +typedef PB_BYTES_ARRAY_T(32) meshtastic_Config_SecurityConfig_admin_key_t; +typedef struct _meshtastic_Config_SecurityConfig { + /* The public key of the user's device. + Sent out to other nodes on the mesh to allow them to compute a shared secret key. */ + meshtastic_Config_SecurityConfig_public_key_t public_key; + /* The private key of the device. + Used to create a shared key with a remote device. */ + meshtastic_Config_SecurityConfig_private_key_t private_key; + /* The public key authorized to send admin messages to this node. */ + pb_size_t admin_key_count; + meshtastic_Config_SecurityConfig_admin_key_t admin_key[3]; + /* If true, device is considered to be "managed" by a mesh administrator via admin messages + Device is managed by a mesh administrator. */ + bool is_managed; + /* Serial Console over the Stream API." */ + bool serial_enabled; + /* By default we turn off logging as soon as an API client connects (to keep shared serial link quiet). + Output live debug logging over serial or bluetooth is set to true. */ + bool debug_log_api_enabled; + /* Allow incoming device control over the insecure legacy admin channel. */ + bool admin_channel_enabled; +} meshtastic_Config_SecurityConfig; + +/* Blank config request, strictly for getting the session key */ +typedef struct _meshtastic_Config_SessionkeyConfig { + char dummy_field; +} meshtastic_Config_SessionkeyConfig; + +typedef struct _meshtastic_Config { + pb_size_t which_payload_variant; + union { + meshtastic_Config_DeviceConfig device; + meshtastic_Config_PositionConfig position; + meshtastic_Config_PowerConfig power; + meshtastic_Config_NetworkConfig network; + meshtastic_Config_DisplayConfig display; + meshtastic_Config_LoRaConfig lora; + meshtastic_Config_BluetoothConfig bluetooth; + meshtastic_Config_SecurityConfig security; + meshtastic_Config_SessionkeyConfig sessionkey; + meshtastic_DeviceUIConfig device_ui; + } payload_variant; +} meshtastic_Config; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Helper constants for enums */ +#define _meshtastic_Config_DeviceConfig_Role_MIN meshtastic_Config_DeviceConfig_Role_CLIENT +#define _meshtastic_Config_DeviceConfig_Role_MAX meshtastic_Config_DeviceConfig_Role_TAK_TRACKER +#define _meshtastic_Config_DeviceConfig_Role_ARRAYSIZE ((meshtastic_Config_DeviceConfig_Role)(meshtastic_Config_DeviceConfig_Role_TAK_TRACKER+1)) + +#define _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN meshtastic_Config_DeviceConfig_RebroadcastMode_ALL +#define _meshtastic_Config_DeviceConfig_RebroadcastMode_MAX meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY +#define _meshtastic_Config_DeviceConfig_RebroadcastMode_ARRAYSIZE ((meshtastic_Config_DeviceConfig_RebroadcastMode)(meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY+1)) + +#define _meshtastic_Config_PositionConfig_PositionFlags_MIN meshtastic_Config_PositionConfig_PositionFlags_UNSET +#define _meshtastic_Config_PositionConfig_PositionFlags_MAX meshtastic_Config_PositionConfig_PositionFlags_SPEED +#define _meshtastic_Config_PositionConfig_PositionFlags_ARRAYSIZE ((meshtastic_Config_PositionConfig_PositionFlags)(meshtastic_Config_PositionConfig_PositionFlags_SPEED+1)) + +#define _meshtastic_Config_PositionConfig_GpsMode_MIN meshtastic_Config_PositionConfig_GpsMode_DISABLED +#define _meshtastic_Config_PositionConfig_GpsMode_MAX meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT +#define _meshtastic_Config_PositionConfig_GpsMode_ARRAYSIZE ((meshtastic_Config_PositionConfig_GpsMode)(meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT+1)) + +#define _meshtastic_Config_NetworkConfig_AddressMode_MIN meshtastic_Config_NetworkConfig_AddressMode_DHCP +#define _meshtastic_Config_NetworkConfig_AddressMode_MAX meshtastic_Config_NetworkConfig_AddressMode_STATIC +#define _meshtastic_Config_NetworkConfig_AddressMode_ARRAYSIZE ((meshtastic_Config_NetworkConfig_AddressMode)(meshtastic_Config_NetworkConfig_AddressMode_STATIC+1)) + +#define _meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MIN meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DEC +#define _meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MAX meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR +#define _meshtastic_Config_DisplayConfig_GpsCoordinateFormat_ARRAYSIZE ((meshtastic_Config_DisplayConfig_GpsCoordinateFormat)(meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR+1)) + +#define _meshtastic_Config_DisplayConfig_DisplayUnits_MIN meshtastic_Config_DisplayConfig_DisplayUnits_METRIC +#define _meshtastic_Config_DisplayConfig_DisplayUnits_MAX meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL +#define _meshtastic_Config_DisplayConfig_DisplayUnits_ARRAYSIZE ((meshtastic_Config_DisplayConfig_DisplayUnits)(meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL+1)) + +#define _meshtastic_Config_DisplayConfig_OledType_MIN meshtastic_Config_DisplayConfig_OledType_OLED_AUTO +#define _meshtastic_Config_DisplayConfig_OledType_MAX meshtastic_Config_DisplayConfig_OledType_OLED_SH1107 +#define _meshtastic_Config_DisplayConfig_OledType_ARRAYSIZE ((meshtastic_Config_DisplayConfig_OledType)(meshtastic_Config_DisplayConfig_OledType_OLED_SH1107+1)) + +#define _meshtastic_Config_DisplayConfig_DisplayMode_MIN meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT +#define _meshtastic_Config_DisplayConfig_DisplayMode_MAX meshtastic_Config_DisplayConfig_DisplayMode_COLOR +#define _meshtastic_Config_DisplayConfig_DisplayMode_ARRAYSIZE ((meshtastic_Config_DisplayConfig_DisplayMode)(meshtastic_Config_DisplayConfig_DisplayMode_COLOR+1)) + +#define _meshtastic_Config_DisplayConfig_CompassOrientation_MIN meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0 +#define _meshtastic_Config_DisplayConfig_CompassOrientation_MAX meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED +#define _meshtastic_Config_DisplayConfig_CompassOrientation_ARRAYSIZE ((meshtastic_Config_DisplayConfig_CompassOrientation)(meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED+1)) + +#define _meshtastic_Config_LoRaConfig_RegionCode_MIN meshtastic_Config_LoRaConfig_RegionCode_UNSET +#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_PH_915 +#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_PH_915+1)) + +#define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST +#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO +#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO+1)) + +#define _meshtastic_Config_BluetoothConfig_PairingMode_MIN meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN +#define _meshtastic_Config_BluetoothConfig_PairingMode_MAX meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN +#define _meshtastic_Config_BluetoothConfig_PairingMode_ARRAYSIZE ((meshtastic_Config_BluetoothConfig_PairingMode)(meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN+1)) + + +#define meshtastic_Config_DeviceConfig_role_ENUMTYPE meshtastic_Config_DeviceConfig_Role +#define meshtastic_Config_DeviceConfig_rebroadcast_mode_ENUMTYPE meshtastic_Config_DeviceConfig_RebroadcastMode + +#define meshtastic_Config_PositionConfig_gps_mode_ENUMTYPE meshtastic_Config_PositionConfig_GpsMode + + +#define meshtastic_Config_NetworkConfig_address_mode_ENUMTYPE meshtastic_Config_NetworkConfig_AddressMode + + +#define meshtastic_Config_DisplayConfig_gps_format_ENUMTYPE meshtastic_Config_DisplayConfig_GpsCoordinateFormat +#define meshtastic_Config_DisplayConfig_units_ENUMTYPE meshtastic_Config_DisplayConfig_DisplayUnits +#define meshtastic_Config_DisplayConfig_oled_ENUMTYPE meshtastic_Config_DisplayConfig_OledType +#define meshtastic_Config_DisplayConfig_displaymode_ENUMTYPE meshtastic_Config_DisplayConfig_DisplayMode +#define meshtastic_Config_DisplayConfig_compass_orientation_ENUMTYPE meshtastic_Config_DisplayConfig_CompassOrientation + +#define meshtastic_Config_LoRaConfig_modem_preset_ENUMTYPE meshtastic_Config_LoRaConfig_ModemPreset +#define meshtastic_Config_LoRaConfig_region_ENUMTYPE meshtastic_Config_LoRaConfig_RegionCode + +#define meshtastic_Config_BluetoothConfig_mode_ENUMTYPE meshtastic_Config_BluetoothConfig_PairingMode + + + + +/* Initializer values for message structs */ +#define meshtastic_Config_init_default {0, {meshtastic_Config_DeviceConfig_init_default}} +#define meshtastic_Config_DeviceConfig_init_default {_meshtastic_Config_DeviceConfig_Role_MIN, 0, 0, 0, _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN, 0, 0, 0, 0, "", 0} +#define meshtastic_Config_PositionConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, _meshtastic_Config_PositionConfig_GpsMode_MIN} +#define meshtastic_Config_PowerConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0} +#define meshtastic_Config_NetworkConfig_init_default {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_default, ""} +#define meshtastic_Config_NetworkConfig_IpV4Config_init_default {0, 0, 0, 0} +#define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN} +#define meshtastic_Config_LoRaConfig_init_default {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0} +#define meshtastic_Config_BluetoothConfig_init_default {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0} +#define meshtastic_Config_SecurityConfig_init_default {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0} +#define meshtastic_Config_SessionkeyConfig_init_default {0} +#define meshtastic_Config_init_zero {0, {meshtastic_Config_DeviceConfig_init_zero}} +#define meshtastic_Config_DeviceConfig_init_zero {_meshtastic_Config_DeviceConfig_Role_MIN, 0, 0, 0, _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN, 0, 0, 0, 0, "", 0} +#define meshtastic_Config_PositionConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, _meshtastic_Config_PositionConfig_GpsMode_MIN} +#define meshtastic_Config_PowerConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0} +#define meshtastic_Config_NetworkConfig_init_zero {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_zero, ""} +#define meshtastic_Config_NetworkConfig_IpV4Config_init_zero {0, 0, 0, 0} +#define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN} +#define meshtastic_Config_LoRaConfig_init_zero {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0} +#define meshtastic_Config_BluetoothConfig_init_zero {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0} +#define meshtastic_Config_SecurityConfig_init_zero {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0} +#define meshtastic_Config_SessionkeyConfig_init_zero {0} + +/* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_Config_DeviceConfig_role_tag 1 +#define meshtastic_Config_DeviceConfig_serial_enabled_tag 2 +#define meshtastic_Config_DeviceConfig_button_gpio_tag 4 +#define meshtastic_Config_DeviceConfig_buzzer_gpio_tag 5 +#define meshtastic_Config_DeviceConfig_rebroadcast_mode_tag 6 +#define meshtastic_Config_DeviceConfig_node_info_broadcast_secs_tag 7 +#define meshtastic_Config_DeviceConfig_double_tap_as_button_press_tag 8 +#define meshtastic_Config_DeviceConfig_is_managed_tag 9 +#define meshtastic_Config_DeviceConfig_disable_triple_click_tag 10 +#define meshtastic_Config_DeviceConfig_tzdef_tag 11 +#define meshtastic_Config_DeviceConfig_led_heartbeat_disabled_tag 12 +#define meshtastic_Config_PositionConfig_position_broadcast_secs_tag 1 +#define meshtastic_Config_PositionConfig_position_broadcast_smart_enabled_tag 2 +#define meshtastic_Config_PositionConfig_fixed_position_tag 3 +#define meshtastic_Config_PositionConfig_gps_enabled_tag 4 +#define meshtastic_Config_PositionConfig_gps_update_interval_tag 5 +#define meshtastic_Config_PositionConfig_gps_attempt_time_tag 6 +#define meshtastic_Config_PositionConfig_position_flags_tag 7 +#define meshtastic_Config_PositionConfig_rx_gpio_tag 8 +#define meshtastic_Config_PositionConfig_tx_gpio_tag 9 +#define meshtastic_Config_PositionConfig_broadcast_smart_minimum_distance_tag 10 +#define meshtastic_Config_PositionConfig_broadcast_smart_minimum_interval_secs_tag 11 +#define meshtastic_Config_PositionConfig_gps_en_gpio_tag 12 +#define meshtastic_Config_PositionConfig_gps_mode_tag 13 +#define meshtastic_Config_PowerConfig_is_power_saving_tag 1 +#define meshtastic_Config_PowerConfig_on_battery_shutdown_after_secs_tag 2 +#define meshtastic_Config_PowerConfig_adc_multiplier_override_tag 3 +#define meshtastic_Config_PowerConfig_wait_bluetooth_secs_tag 4 +#define meshtastic_Config_PowerConfig_sds_secs_tag 6 +#define meshtastic_Config_PowerConfig_ls_secs_tag 7 +#define meshtastic_Config_PowerConfig_min_wake_secs_tag 8 +#define meshtastic_Config_PowerConfig_device_battery_ina_address_tag 9 +#define meshtastic_Config_PowerConfig_powermon_enables_tag 32 +#define meshtastic_Config_NetworkConfig_IpV4Config_ip_tag 1 +#define meshtastic_Config_NetworkConfig_IpV4Config_gateway_tag 2 +#define meshtastic_Config_NetworkConfig_IpV4Config_subnet_tag 3 +#define meshtastic_Config_NetworkConfig_IpV4Config_dns_tag 4 +#define meshtastic_Config_NetworkConfig_wifi_enabled_tag 1 +#define meshtastic_Config_NetworkConfig_wifi_ssid_tag 3 +#define meshtastic_Config_NetworkConfig_wifi_psk_tag 4 +#define meshtastic_Config_NetworkConfig_ntp_server_tag 5 +#define meshtastic_Config_NetworkConfig_eth_enabled_tag 6 +#define meshtastic_Config_NetworkConfig_address_mode_tag 7 +#define meshtastic_Config_NetworkConfig_ipv4_config_tag 8 +#define meshtastic_Config_NetworkConfig_rsyslog_server_tag 9 +#define meshtastic_Config_DisplayConfig_screen_on_secs_tag 1 +#define meshtastic_Config_DisplayConfig_gps_format_tag 2 +#define meshtastic_Config_DisplayConfig_auto_screen_carousel_secs_tag 3 +#define meshtastic_Config_DisplayConfig_compass_north_top_tag 4 +#define meshtastic_Config_DisplayConfig_flip_screen_tag 5 +#define meshtastic_Config_DisplayConfig_units_tag 6 +#define meshtastic_Config_DisplayConfig_oled_tag 7 +#define meshtastic_Config_DisplayConfig_displaymode_tag 8 +#define meshtastic_Config_DisplayConfig_heading_bold_tag 9 +#define meshtastic_Config_DisplayConfig_wake_on_tap_or_motion_tag 10 +#define meshtastic_Config_DisplayConfig_compass_orientation_tag 11 +#define meshtastic_Config_LoRaConfig_use_preset_tag 1 +#define meshtastic_Config_LoRaConfig_modem_preset_tag 2 +#define meshtastic_Config_LoRaConfig_bandwidth_tag 3 +#define meshtastic_Config_LoRaConfig_spread_factor_tag 4 +#define meshtastic_Config_LoRaConfig_coding_rate_tag 5 +#define meshtastic_Config_LoRaConfig_frequency_offset_tag 6 +#define meshtastic_Config_LoRaConfig_region_tag 7 +#define meshtastic_Config_LoRaConfig_hop_limit_tag 8 +#define meshtastic_Config_LoRaConfig_tx_enabled_tag 9 +#define meshtastic_Config_LoRaConfig_tx_power_tag 10 +#define meshtastic_Config_LoRaConfig_channel_num_tag 11 +#define meshtastic_Config_LoRaConfig_override_duty_cycle_tag 12 +#define meshtastic_Config_LoRaConfig_sx126x_rx_boosted_gain_tag 13 +#define meshtastic_Config_LoRaConfig_override_frequency_tag 14 +#define meshtastic_Config_LoRaConfig_pa_fan_disabled_tag 15 +#define meshtastic_Config_LoRaConfig_ignore_incoming_tag 103 +#define meshtastic_Config_LoRaConfig_ignore_mqtt_tag 104 +#define meshtastic_Config_LoRaConfig_config_ok_to_mqtt_tag 105 +#define meshtastic_Config_BluetoothConfig_enabled_tag 1 +#define meshtastic_Config_BluetoothConfig_mode_tag 2 +#define meshtastic_Config_BluetoothConfig_fixed_pin_tag 3 +#define meshtastic_Config_SecurityConfig_public_key_tag 1 +#define meshtastic_Config_SecurityConfig_private_key_tag 2 +#define meshtastic_Config_SecurityConfig_admin_key_tag 3 +#define meshtastic_Config_SecurityConfig_is_managed_tag 4 +#define meshtastic_Config_SecurityConfig_serial_enabled_tag 5 +#define meshtastic_Config_SecurityConfig_debug_log_api_enabled_tag 6 +#define meshtastic_Config_SecurityConfig_admin_channel_enabled_tag 8 +#define meshtastic_Config_device_tag 1 +#define meshtastic_Config_position_tag 2 +#define meshtastic_Config_power_tag 3 +#define meshtastic_Config_network_tag 4 +#define meshtastic_Config_display_tag 5 +#define meshtastic_Config_lora_tag 6 +#define meshtastic_Config_bluetooth_tag 7 +#define meshtastic_Config_security_tag 8 +#define meshtastic_Config_sessionkey_tag 9 +#define meshtastic_Config_device_ui_tag 10 + +/* Struct field encoding specification for nanopb */ +#define meshtastic_Config_FIELDLIST(X, a) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,device,payload_variant.device), 1) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,position,payload_variant.position), 2) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,power,payload_variant.power), 3) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,network,payload_variant.network), 4) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,display,payload_variant.display), 5) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,lora,payload_variant.lora), 6) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,bluetooth,payload_variant.bluetooth), 7) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,security,payload_variant.security), 8) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,sessionkey,payload_variant.sessionkey), 9) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,device_ui,payload_variant.device_ui), 10) +#define meshtastic_Config_CALLBACK NULL +#define meshtastic_Config_DEFAULT NULL +#define meshtastic_Config_payload_variant_device_MSGTYPE meshtastic_Config_DeviceConfig +#define meshtastic_Config_payload_variant_position_MSGTYPE meshtastic_Config_PositionConfig +#define meshtastic_Config_payload_variant_power_MSGTYPE meshtastic_Config_PowerConfig +#define meshtastic_Config_payload_variant_network_MSGTYPE meshtastic_Config_NetworkConfig +#define meshtastic_Config_payload_variant_display_MSGTYPE meshtastic_Config_DisplayConfig +#define meshtastic_Config_payload_variant_lora_MSGTYPE meshtastic_Config_LoRaConfig +#define meshtastic_Config_payload_variant_bluetooth_MSGTYPE meshtastic_Config_BluetoothConfig +#define meshtastic_Config_payload_variant_security_MSGTYPE meshtastic_Config_SecurityConfig +#define meshtastic_Config_payload_variant_sessionkey_MSGTYPE meshtastic_Config_SessionkeyConfig +#define meshtastic_Config_payload_variant_device_ui_MSGTYPE meshtastic_DeviceUIConfig + +#define meshtastic_Config_DeviceConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, role, 1) \ +X(a, STATIC, SINGULAR, BOOL, serial_enabled, 2) \ +X(a, STATIC, SINGULAR, UINT32, button_gpio, 4) \ +X(a, STATIC, SINGULAR, UINT32, buzzer_gpio, 5) \ +X(a, STATIC, SINGULAR, UENUM, rebroadcast_mode, 6) \ +X(a, STATIC, SINGULAR, UINT32, node_info_broadcast_secs, 7) \ +X(a, STATIC, SINGULAR, BOOL, double_tap_as_button_press, 8) \ +X(a, STATIC, SINGULAR, BOOL, is_managed, 9) \ +X(a, STATIC, SINGULAR, BOOL, disable_triple_click, 10) \ +X(a, STATIC, SINGULAR, STRING, tzdef, 11) \ +X(a, STATIC, SINGULAR, BOOL, led_heartbeat_disabled, 12) +#define meshtastic_Config_DeviceConfig_CALLBACK NULL +#define meshtastic_Config_DeviceConfig_DEFAULT NULL + +#define meshtastic_Config_PositionConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, position_broadcast_secs, 1) \ +X(a, STATIC, SINGULAR, BOOL, position_broadcast_smart_enabled, 2) \ +X(a, STATIC, SINGULAR, BOOL, fixed_position, 3) \ +X(a, STATIC, SINGULAR, BOOL, gps_enabled, 4) \ +X(a, STATIC, SINGULAR, UINT32, gps_update_interval, 5) \ +X(a, STATIC, SINGULAR, UINT32, gps_attempt_time, 6) \ +X(a, STATIC, SINGULAR, UINT32, position_flags, 7) \ +X(a, STATIC, SINGULAR, UINT32, rx_gpio, 8) \ +X(a, STATIC, SINGULAR, UINT32, tx_gpio, 9) \ +X(a, STATIC, SINGULAR, UINT32, broadcast_smart_minimum_distance, 10) \ +X(a, STATIC, SINGULAR, UINT32, broadcast_smart_minimum_interval_secs, 11) \ +X(a, STATIC, SINGULAR, UINT32, gps_en_gpio, 12) \ +X(a, STATIC, SINGULAR, UENUM, gps_mode, 13) +#define meshtastic_Config_PositionConfig_CALLBACK NULL +#define meshtastic_Config_PositionConfig_DEFAULT NULL + +#define meshtastic_Config_PowerConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, is_power_saving, 1) \ +X(a, STATIC, SINGULAR, UINT32, on_battery_shutdown_after_secs, 2) \ +X(a, STATIC, SINGULAR, FLOAT, adc_multiplier_override, 3) \ +X(a, STATIC, SINGULAR, UINT32, wait_bluetooth_secs, 4) \ +X(a, STATIC, SINGULAR, UINT32, sds_secs, 6) \ +X(a, STATIC, SINGULAR, UINT32, ls_secs, 7) \ +X(a, STATIC, SINGULAR, UINT32, min_wake_secs, 8) \ +X(a, STATIC, SINGULAR, UINT32, device_battery_ina_address, 9) \ +X(a, STATIC, SINGULAR, UINT64, powermon_enables, 32) +#define meshtastic_Config_PowerConfig_CALLBACK NULL +#define meshtastic_Config_PowerConfig_DEFAULT NULL + +#define meshtastic_Config_NetworkConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, wifi_enabled, 1) \ +X(a, STATIC, SINGULAR, STRING, wifi_ssid, 3) \ +X(a, STATIC, SINGULAR, STRING, wifi_psk, 4) \ +X(a, STATIC, SINGULAR, STRING, ntp_server, 5) \ +X(a, STATIC, SINGULAR, BOOL, eth_enabled, 6) \ +X(a, STATIC, SINGULAR, UENUM, address_mode, 7) \ +X(a, STATIC, OPTIONAL, MESSAGE, ipv4_config, 8) \ +X(a, STATIC, SINGULAR, STRING, rsyslog_server, 9) +#define meshtastic_Config_NetworkConfig_CALLBACK NULL +#define meshtastic_Config_NetworkConfig_DEFAULT NULL +#define meshtastic_Config_NetworkConfig_ipv4_config_MSGTYPE meshtastic_Config_NetworkConfig_IpV4Config + +#define meshtastic_Config_NetworkConfig_IpV4Config_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, FIXED32, ip, 1) \ +X(a, STATIC, SINGULAR, FIXED32, gateway, 2) \ +X(a, STATIC, SINGULAR, FIXED32, subnet, 3) \ +X(a, STATIC, SINGULAR, FIXED32, dns, 4) +#define meshtastic_Config_NetworkConfig_IpV4Config_CALLBACK NULL +#define meshtastic_Config_NetworkConfig_IpV4Config_DEFAULT NULL + +#define meshtastic_Config_DisplayConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, screen_on_secs, 1) \ +X(a, STATIC, SINGULAR, UENUM, gps_format, 2) \ +X(a, STATIC, SINGULAR, UINT32, auto_screen_carousel_secs, 3) \ +X(a, STATIC, SINGULAR, BOOL, compass_north_top, 4) \ +X(a, STATIC, SINGULAR, BOOL, flip_screen, 5) \ +X(a, STATIC, SINGULAR, UENUM, units, 6) \ +X(a, STATIC, SINGULAR, UENUM, oled, 7) \ +X(a, STATIC, SINGULAR, UENUM, displaymode, 8) \ +X(a, STATIC, SINGULAR, BOOL, heading_bold, 9) \ +X(a, STATIC, SINGULAR, BOOL, wake_on_tap_or_motion, 10) \ +X(a, STATIC, SINGULAR, UENUM, compass_orientation, 11) +#define meshtastic_Config_DisplayConfig_CALLBACK NULL +#define meshtastic_Config_DisplayConfig_DEFAULT NULL + +#define meshtastic_Config_LoRaConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, use_preset, 1) \ +X(a, STATIC, SINGULAR, UENUM, modem_preset, 2) \ +X(a, STATIC, SINGULAR, UINT32, bandwidth, 3) \ +X(a, STATIC, SINGULAR, UINT32, spread_factor, 4) \ +X(a, STATIC, SINGULAR, UINT32, coding_rate, 5) \ +X(a, STATIC, SINGULAR, FLOAT, frequency_offset, 6) \ +X(a, STATIC, SINGULAR, UENUM, region, 7) \ +X(a, STATIC, SINGULAR, UINT32, hop_limit, 8) \ +X(a, STATIC, SINGULAR, BOOL, tx_enabled, 9) \ +X(a, STATIC, SINGULAR, INT32, tx_power, 10) \ +X(a, STATIC, SINGULAR, UINT32, channel_num, 11) \ +X(a, STATIC, SINGULAR, BOOL, override_duty_cycle, 12) \ +X(a, STATIC, SINGULAR, BOOL, sx126x_rx_boosted_gain, 13) \ +X(a, STATIC, SINGULAR, FLOAT, override_frequency, 14) \ +X(a, STATIC, SINGULAR, BOOL, pa_fan_disabled, 15) \ +X(a, STATIC, REPEATED, UINT32, ignore_incoming, 103) \ +X(a, STATIC, SINGULAR, BOOL, ignore_mqtt, 104) \ +X(a, STATIC, SINGULAR, BOOL, config_ok_to_mqtt, 105) +#define meshtastic_Config_LoRaConfig_CALLBACK NULL +#define meshtastic_Config_LoRaConfig_DEFAULT NULL + +#define meshtastic_Config_BluetoothConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, enabled, 1) \ +X(a, STATIC, SINGULAR, UENUM, mode, 2) \ +X(a, STATIC, SINGULAR, UINT32, fixed_pin, 3) +#define meshtastic_Config_BluetoothConfig_CALLBACK NULL +#define meshtastic_Config_BluetoothConfig_DEFAULT NULL + +#define meshtastic_Config_SecurityConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BYTES, public_key, 1) \ +X(a, STATIC, SINGULAR, BYTES, private_key, 2) \ +X(a, STATIC, REPEATED, BYTES, admin_key, 3) \ +X(a, STATIC, SINGULAR, BOOL, is_managed, 4) \ +X(a, STATIC, SINGULAR, BOOL, serial_enabled, 5) \ +X(a, STATIC, SINGULAR, BOOL, debug_log_api_enabled, 6) \ +X(a, STATIC, SINGULAR, BOOL, admin_channel_enabled, 8) +#define meshtastic_Config_SecurityConfig_CALLBACK NULL +#define meshtastic_Config_SecurityConfig_DEFAULT NULL + +#define meshtastic_Config_SessionkeyConfig_FIELDLIST(X, a) \ + +#define meshtastic_Config_SessionkeyConfig_CALLBACK NULL +#define meshtastic_Config_SessionkeyConfig_DEFAULT NULL + +extern const pb_msgdesc_t meshtastic_Config_msg; +extern const pb_msgdesc_t meshtastic_Config_DeviceConfig_msg; +extern const pb_msgdesc_t meshtastic_Config_PositionConfig_msg; +extern const pb_msgdesc_t meshtastic_Config_PowerConfig_msg; +extern const pb_msgdesc_t meshtastic_Config_NetworkConfig_msg; +extern const pb_msgdesc_t meshtastic_Config_NetworkConfig_IpV4Config_msg; +extern const pb_msgdesc_t meshtastic_Config_DisplayConfig_msg; +extern const pb_msgdesc_t meshtastic_Config_LoRaConfig_msg; +extern const pb_msgdesc_t meshtastic_Config_BluetoothConfig_msg; +extern const pb_msgdesc_t meshtastic_Config_SecurityConfig_msg; +extern const pb_msgdesc_t meshtastic_Config_SessionkeyConfig_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define meshtastic_Config_fields &meshtastic_Config_msg +#define meshtastic_Config_DeviceConfig_fields &meshtastic_Config_DeviceConfig_msg +#define meshtastic_Config_PositionConfig_fields &meshtastic_Config_PositionConfig_msg +#define meshtastic_Config_PowerConfig_fields &meshtastic_Config_PowerConfig_msg +#define meshtastic_Config_NetworkConfig_fields &meshtastic_Config_NetworkConfig_msg +#define meshtastic_Config_NetworkConfig_IpV4Config_fields &meshtastic_Config_NetworkConfig_IpV4Config_msg +#define meshtastic_Config_DisplayConfig_fields &meshtastic_Config_DisplayConfig_msg +#define meshtastic_Config_LoRaConfig_fields &meshtastic_Config_LoRaConfig_msg +#define meshtastic_Config_BluetoothConfig_fields &meshtastic_Config_BluetoothConfig_msg +#define meshtastic_Config_SecurityConfig_fields &meshtastic_Config_SecurityConfig_msg +#define meshtastic_Config_SessionkeyConfig_fields &meshtastic_Config_SessionkeyConfig_msg + +/* Maximum encoded size of messages (where known) */ +#define MESHTASTIC_MESHTASTIC_CONFIG_PB_H_MAX_SIZE meshtastic_Config_size +#define meshtastic_Config_BluetoothConfig_size 10 +#define meshtastic_Config_DeviceConfig_size 98 +#define meshtastic_Config_DisplayConfig_size 30 +#define meshtastic_Config_LoRaConfig_size 85 +#define meshtastic_Config_NetworkConfig_IpV4Config_size 20 +#define meshtastic_Config_NetworkConfig_size 196 +#define meshtastic_Config_PositionConfig_size 62 +#define meshtastic_Config_PowerConfig_size 52 +#define meshtastic_Config_SecurityConfig_size 178 +#define meshtastic_Config_SessionkeyConfig_size 0 +#define meshtastic_Config_size 199 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/mesh/generated/meshtastic/connection_status.pb.cpp b/src/mesh/generated/meshtastic/connection_status.pb.cpp new file mode 100644 index 0000000..d1495bb --- /dev/null +++ b/src/mesh/generated/meshtastic/connection_status.pb.cpp @@ -0,0 +1,27 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9 */ + +#include "meshtastic/connection_status.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(meshtastic_DeviceConnectionStatus, meshtastic_DeviceConnectionStatus, AUTO) + + +PB_BIND(meshtastic_WifiConnectionStatus, meshtastic_WifiConnectionStatus, AUTO) + + +PB_BIND(meshtastic_EthernetConnectionStatus, meshtastic_EthernetConnectionStatus, AUTO) + + +PB_BIND(meshtastic_NetworkConnectionStatus, meshtastic_NetworkConnectionStatus, AUTO) + + +PB_BIND(meshtastic_BluetoothConnectionStatus, meshtastic_BluetoothConnectionStatus, AUTO) + + +PB_BIND(meshtastic_SerialConnectionStatus, meshtastic_SerialConnectionStatus, AUTO) + + + diff --git a/src/mesh/generated/meshtastic/connection_status.pb.h b/src/mesh/generated/meshtastic/connection_status.pb.h new file mode 100644 index 0000000..c433e37 --- /dev/null +++ b/src/mesh/generated/meshtastic/connection_status.pb.h @@ -0,0 +1,190 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9 */ + +#ifndef PB_MESHTASTIC_MESHTASTIC_CONNECTION_STATUS_PB_H_INCLUDED +#define PB_MESHTASTIC_MESHTASTIC_CONNECTION_STATUS_PB_H_INCLUDED +#include + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Struct definitions */ +/* Ethernet or WiFi connection status */ +typedef struct _meshtastic_NetworkConnectionStatus { + /* IP address of device */ + uint32_t ip_address; + /* Whether the device has an active connection or not */ + bool is_connected; + /* Whether the device has an active connection to an MQTT broker or not */ + bool is_mqtt_connected; + /* Whether the device is actively remote syslogging or not */ + bool is_syslog_connected; +} meshtastic_NetworkConnectionStatus; + +/* WiFi connection status */ +typedef struct _meshtastic_WifiConnectionStatus { + /* Connection status */ + bool has_status; + meshtastic_NetworkConnectionStatus status; + /* WiFi access point SSID */ + char ssid[33]; + /* RSSI of wireless connection */ + int32_t rssi; +} meshtastic_WifiConnectionStatus; + +/* Ethernet connection status */ +typedef struct _meshtastic_EthernetConnectionStatus { + /* Connection status */ + bool has_status; + meshtastic_NetworkConnectionStatus status; +} meshtastic_EthernetConnectionStatus; + +/* Bluetooth connection status */ +typedef struct _meshtastic_BluetoothConnectionStatus { + /* The pairing PIN for bluetooth */ + uint32_t pin; + /* RSSI of bluetooth connection */ + int32_t rssi; + /* Whether the device has an active connection or not */ + bool is_connected; +} meshtastic_BluetoothConnectionStatus; + +/* Serial connection status */ +typedef struct _meshtastic_SerialConnectionStatus { + /* Serial baud rate */ + uint32_t baud; + /* Whether the device has an active connection or not */ + bool is_connected; +} meshtastic_SerialConnectionStatus; + +typedef struct _meshtastic_DeviceConnectionStatus { + /* WiFi Status */ + bool has_wifi; + meshtastic_WifiConnectionStatus wifi; + /* WiFi Status */ + bool has_ethernet; + meshtastic_EthernetConnectionStatus ethernet; + /* Bluetooth Status */ + bool has_bluetooth; + meshtastic_BluetoothConnectionStatus bluetooth; + /* Serial Status */ + bool has_serial; + meshtastic_SerialConnectionStatus serial; +} meshtastic_DeviceConnectionStatus; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initializer values for message structs */ +#define meshtastic_DeviceConnectionStatus_init_default {false, meshtastic_WifiConnectionStatus_init_default, false, meshtastic_EthernetConnectionStatus_init_default, false, meshtastic_BluetoothConnectionStatus_init_default, false, meshtastic_SerialConnectionStatus_init_default} +#define meshtastic_WifiConnectionStatus_init_default {false, meshtastic_NetworkConnectionStatus_init_default, "", 0} +#define meshtastic_EthernetConnectionStatus_init_default {false, meshtastic_NetworkConnectionStatus_init_default} +#define meshtastic_NetworkConnectionStatus_init_default {0, 0, 0, 0} +#define meshtastic_BluetoothConnectionStatus_init_default {0, 0, 0} +#define meshtastic_SerialConnectionStatus_init_default {0, 0} +#define meshtastic_DeviceConnectionStatus_init_zero {false, meshtastic_WifiConnectionStatus_init_zero, false, meshtastic_EthernetConnectionStatus_init_zero, false, meshtastic_BluetoothConnectionStatus_init_zero, false, meshtastic_SerialConnectionStatus_init_zero} +#define meshtastic_WifiConnectionStatus_init_zero {false, meshtastic_NetworkConnectionStatus_init_zero, "", 0} +#define meshtastic_EthernetConnectionStatus_init_zero {false, meshtastic_NetworkConnectionStatus_init_zero} +#define meshtastic_NetworkConnectionStatus_init_zero {0, 0, 0, 0} +#define meshtastic_BluetoothConnectionStatus_init_zero {0, 0, 0} +#define meshtastic_SerialConnectionStatus_init_zero {0, 0} + +/* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_NetworkConnectionStatus_ip_address_tag 1 +#define meshtastic_NetworkConnectionStatus_is_connected_tag 2 +#define meshtastic_NetworkConnectionStatus_is_mqtt_connected_tag 3 +#define meshtastic_NetworkConnectionStatus_is_syslog_connected_tag 4 +#define meshtastic_WifiConnectionStatus_status_tag 1 +#define meshtastic_WifiConnectionStatus_ssid_tag 2 +#define meshtastic_WifiConnectionStatus_rssi_tag 3 +#define meshtastic_EthernetConnectionStatus_status_tag 1 +#define meshtastic_BluetoothConnectionStatus_pin_tag 1 +#define meshtastic_BluetoothConnectionStatus_rssi_tag 2 +#define meshtastic_BluetoothConnectionStatus_is_connected_tag 3 +#define meshtastic_SerialConnectionStatus_baud_tag 1 +#define meshtastic_SerialConnectionStatus_is_connected_tag 2 +#define meshtastic_DeviceConnectionStatus_wifi_tag 1 +#define meshtastic_DeviceConnectionStatus_ethernet_tag 2 +#define meshtastic_DeviceConnectionStatus_bluetooth_tag 3 +#define meshtastic_DeviceConnectionStatus_serial_tag 4 + +/* Struct field encoding specification for nanopb */ +#define meshtastic_DeviceConnectionStatus_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, MESSAGE, wifi, 1) \ +X(a, STATIC, OPTIONAL, MESSAGE, ethernet, 2) \ +X(a, STATIC, OPTIONAL, MESSAGE, bluetooth, 3) \ +X(a, STATIC, OPTIONAL, MESSAGE, serial, 4) +#define meshtastic_DeviceConnectionStatus_CALLBACK NULL +#define meshtastic_DeviceConnectionStatus_DEFAULT NULL +#define meshtastic_DeviceConnectionStatus_wifi_MSGTYPE meshtastic_WifiConnectionStatus +#define meshtastic_DeviceConnectionStatus_ethernet_MSGTYPE meshtastic_EthernetConnectionStatus +#define meshtastic_DeviceConnectionStatus_bluetooth_MSGTYPE meshtastic_BluetoothConnectionStatus +#define meshtastic_DeviceConnectionStatus_serial_MSGTYPE meshtastic_SerialConnectionStatus + +#define meshtastic_WifiConnectionStatus_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, MESSAGE, status, 1) \ +X(a, STATIC, SINGULAR, STRING, ssid, 2) \ +X(a, STATIC, SINGULAR, INT32, rssi, 3) +#define meshtastic_WifiConnectionStatus_CALLBACK NULL +#define meshtastic_WifiConnectionStatus_DEFAULT NULL +#define meshtastic_WifiConnectionStatus_status_MSGTYPE meshtastic_NetworkConnectionStatus + +#define meshtastic_EthernetConnectionStatus_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, MESSAGE, status, 1) +#define meshtastic_EthernetConnectionStatus_CALLBACK NULL +#define meshtastic_EthernetConnectionStatus_DEFAULT NULL +#define meshtastic_EthernetConnectionStatus_status_MSGTYPE meshtastic_NetworkConnectionStatus + +#define meshtastic_NetworkConnectionStatus_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, FIXED32, ip_address, 1) \ +X(a, STATIC, SINGULAR, BOOL, is_connected, 2) \ +X(a, STATIC, SINGULAR, BOOL, is_mqtt_connected, 3) \ +X(a, STATIC, SINGULAR, BOOL, is_syslog_connected, 4) +#define meshtastic_NetworkConnectionStatus_CALLBACK NULL +#define meshtastic_NetworkConnectionStatus_DEFAULT NULL + +#define meshtastic_BluetoothConnectionStatus_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, pin, 1) \ +X(a, STATIC, SINGULAR, INT32, rssi, 2) \ +X(a, STATIC, SINGULAR, BOOL, is_connected, 3) +#define meshtastic_BluetoothConnectionStatus_CALLBACK NULL +#define meshtastic_BluetoothConnectionStatus_DEFAULT NULL + +#define meshtastic_SerialConnectionStatus_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, baud, 1) \ +X(a, STATIC, SINGULAR, BOOL, is_connected, 2) +#define meshtastic_SerialConnectionStatus_CALLBACK NULL +#define meshtastic_SerialConnectionStatus_DEFAULT NULL + +extern const pb_msgdesc_t meshtastic_DeviceConnectionStatus_msg; +extern const pb_msgdesc_t meshtastic_WifiConnectionStatus_msg; +extern const pb_msgdesc_t meshtastic_EthernetConnectionStatus_msg; +extern const pb_msgdesc_t meshtastic_NetworkConnectionStatus_msg; +extern const pb_msgdesc_t meshtastic_BluetoothConnectionStatus_msg; +extern const pb_msgdesc_t meshtastic_SerialConnectionStatus_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define meshtastic_DeviceConnectionStatus_fields &meshtastic_DeviceConnectionStatus_msg +#define meshtastic_WifiConnectionStatus_fields &meshtastic_WifiConnectionStatus_msg +#define meshtastic_EthernetConnectionStatus_fields &meshtastic_EthernetConnectionStatus_msg +#define meshtastic_NetworkConnectionStatus_fields &meshtastic_NetworkConnectionStatus_msg +#define meshtastic_BluetoothConnectionStatus_fields &meshtastic_BluetoothConnectionStatus_msg +#define meshtastic_SerialConnectionStatus_fields &meshtastic_SerialConnectionStatus_msg + +/* Maximum encoded size of messages (where known) */ +#define MESHTASTIC_MESHTASTIC_CONNECTION_STATUS_PB_H_MAX_SIZE meshtastic_DeviceConnectionStatus_size +#define meshtastic_BluetoothConnectionStatus_size 19 +#define meshtastic_DeviceConnectionStatus_size 106 +#define meshtastic_EthernetConnectionStatus_size 13 +#define meshtastic_NetworkConnectionStatus_size 11 +#define meshtastic_SerialConnectionStatus_size 8 +#define meshtastic_WifiConnectionStatus_size 58 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/mesh/generated/meshtastic/device_ui.pb.cpp b/src/mesh/generated/meshtastic/device_ui.pb.cpp new file mode 100644 index 0000000..6e0cf0c --- /dev/null +++ b/src/mesh/generated/meshtastic/device_ui.pb.cpp @@ -0,0 +1,22 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9 */ + +#include "meshtastic/device_ui.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(meshtastic_DeviceUIConfig, meshtastic_DeviceUIConfig, AUTO) + + +PB_BIND(meshtastic_NodeFilter, meshtastic_NodeFilter, AUTO) + + +PB_BIND(meshtastic_NodeHighlight, meshtastic_NodeHighlight, AUTO) + + + + + + + diff --git a/src/mesh/generated/meshtastic/device_ui.pb.h b/src/mesh/generated/meshtastic/device_ui.pb.h new file mode 100644 index 0000000..5c1e067 --- /dev/null +++ b/src/mesh/generated/meshtastic/device_ui.pb.h @@ -0,0 +1,226 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9 */ + +#ifndef PB_MESHTASTIC_MESHTASTIC_DEVICE_UI_PB_H_INCLUDED +#define PB_MESHTASTIC_MESHTASTIC_DEVICE_UI_PB_H_INCLUDED +#include + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Enum definitions */ +typedef enum _meshtastic_Theme { + /* Dark */ + meshtastic_Theme_DARK = 0, + /* Light */ + meshtastic_Theme_LIGHT = 1, + /* Red */ + meshtastic_Theme_RED = 2 +} meshtastic_Theme; + +/* Localization */ +typedef enum _meshtastic_Language { + /* English */ + meshtastic_Language_ENGLISH = 0, + /* French */ + meshtastic_Language_FRENCH = 1, + /* German */ + meshtastic_Language_GERMAN = 2, + /* Italian */ + meshtastic_Language_ITALIAN = 3, + /* Portuguese */ + meshtastic_Language_PORTUGUESE = 4, + /* Spanish */ + meshtastic_Language_SPANISH = 5, + /* Swedish */ + meshtastic_Language_SWEDISH = 6, + /* Finnish */ + meshtastic_Language_FINNISH = 7, + /* Polish */ + meshtastic_Language_POLISH = 8, + /* Turkish */ + meshtastic_Language_TURKISH = 9, + /* Serbian */ + meshtastic_Language_SERBIAN = 10, + /* Russian */ + meshtastic_Language_RUSSIAN = 11, + /* Dutch */ + meshtastic_Language_DUTCH = 12, + /* Greek */ + meshtastic_Language_GREEK = 13, + /* Simplified Chinese (experimental) */ + meshtastic_Language_SIMPLIFIED_CHINESE = 30, + /* Traditional Chinese (experimental) */ + meshtastic_Language_TRADITIONAL_CHINESE = 31 +} meshtastic_Language; + +/* Struct definitions */ +typedef struct _meshtastic_NodeFilter { + /* Filter unknown nodes */ + bool unknown_switch; + /* Filter offline nodes */ + bool offline_switch; + /* Filter nodes w/o public key */ + bool public_key_switch; + /* Filter based on hops away */ + int8_t hops_away; + /* Filter nodes w/o position */ + bool position_switch; + /* Filter nodes by matching name string */ + char node_name[16]; +} meshtastic_NodeFilter; + +typedef struct _meshtastic_NodeHighlight { + /* Hightlight nodes w/ active chat */ + bool chat_switch; + /* Highlight nodes w/ position */ + bool position_switch; + /* Highlight nodes w/ telemetry data */ + bool telemetry_switch; + /* Highlight nodes w/ iaq data */ + bool iaq_switch; + /* Highlight nodes by matching name string */ + char node_name[16]; +} meshtastic_NodeHighlight; + +typedef struct _meshtastic_DeviceUIConfig { + /* A version integer used to invalidate saved files when we make incompatible changes. */ + uint32_t version; + /* TFT display brightness 1..255 */ + uint8_t screen_brightness; + /* Screen timeout 0..900 */ + uint16_t screen_timeout; + /* Screen/Settings lock enabled */ + bool screen_lock; + bool settings_lock; + uint32_t pin_code; + /* Color theme */ + meshtastic_Theme theme; + /* Audible message, banner and ring tone */ + bool alert_enabled; + bool banner_enabled; + uint8_t ring_tone_id; + /* Localization */ + meshtastic_Language language; + /* Node list filter */ + bool has_node_filter; + meshtastic_NodeFilter node_filter; + /* Node list highlightening */ + bool has_node_highlight; + meshtastic_NodeHighlight node_highlight; +} meshtastic_DeviceUIConfig; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Helper constants for enums */ +#define _meshtastic_Theme_MIN meshtastic_Theme_DARK +#define _meshtastic_Theme_MAX meshtastic_Theme_RED +#define _meshtastic_Theme_ARRAYSIZE ((meshtastic_Theme)(meshtastic_Theme_RED+1)) + +#define _meshtastic_Language_MIN meshtastic_Language_ENGLISH +#define _meshtastic_Language_MAX meshtastic_Language_TRADITIONAL_CHINESE +#define _meshtastic_Language_ARRAYSIZE ((meshtastic_Language)(meshtastic_Language_TRADITIONAL_CHINESE+1)) + +#define meshtastic_DeviceUIConfig_theme_ENUMTYPE meshtastic_Theme +#define meshtastic_DeviceUIConfig_language_ENUMTYPE meshtastic_Language + + + + +/* Initializer values for message structs */ +#define meshtastic_DeviceUIConfig_init_default {0, 0, 0, 0, 0, 0, _meshtastic_Theme_MIN, 0, 0, 0, _meshtastic_Language_MIN, false, meshtastic_NodeFilter_init_default, false, meshtastic_NodeHighlight_init_default} +#define meshtastic_NodeFilter_init_default {0, 0, 0, 0, 0, ""} +#define meshtastic_NodeHighlight_init_default {0, 0, 0, 0, ""} +#define meshtastic_DeviceUIConfig_init_zero {0, 0, 0, 0, 0, 0, _meshtastic_Theme_MIN, 0, 0, 0, _meshtastic_Language_MIN, false, meshtastic_NodeFilter_init_zero, false, meshtastic_NodeHighlight_init_zero} +#define meshtastic_NodeFilter_init_zero {0, 0, 0, 0, 0, ""} +#define meshtastic_NodeHighlight_init_zero {0, 0, 0, 0, ""} + +/* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_NodeFilter_unknown_switch_tag 1 +#define meshtastic_NodeFilter_offline_switch_tag 2 +#define meshtastic_NodeFilter_public_key_switch_tag 3 +#define meshtastic_NodeFilter_hops_away_tag 4 +#define meshtastic_NodeFilter_position_switch_tag 5 +#define meshtastic_NodeFilter_node_name_tag 6 +#define meshtastic_NodeHighlight_chat_switch_tag 1 +#define meshtastic_NodeHighlight_position_switch_tag 2 +#define meshtastic_NodeHighlight_telemetry_switch_tag 3 +#define meshtastic_NodeHighlight_iaq_switch_tag 4 +#define meshtastic_NodeHighlight_node_name_tag 5 +#define meshtastic_DeviceUIConfig_version_tag 1 +#define meshtastic_DeviceUIConfig_screen_brightness_tag 2 +#define meshtastic_DeviceUIConfig_screen_timeout_tag 3 +#define meshtastic_DeviceUIConfig_screen_lock_tag 4 +#define meshtastic_DeviceUIConfig_settings_lock_tag 5 +#define meshtastic_DeviceUIConfig_pin_code_tag 6 +#define meshtastic_DeviceUIConfig_theme_tag 7 +#define meshtastic_DeviceUIConfig_alert_enabled_tag 8 +#define meshtastic_DeviceUIConfig_banner_enabled_tag 9 +#define meshtastic_DeviceUIConfig_ring_tone_id_tag 10 +#define meshtastic_DeviceUIConfig_language_tag 11 +#define meshtastic_DeviceUIConfig_node_filter_tag 12 +#define meshtastic_DeviceUIConfig_node_highlight_tag 13 + +/* Struct field encoding specification for nanopb */ +#define meshtastic_DeviceUIConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, version, 1) \ +X(a, STATIC, SINGULAR, UINT32, screen_brightness, 2) \ +X(a, STATIC, SINGULAR, UINT32, screen_timeout, 3) \ +X(a, STATIC, SINGULAR, BOOL, screen_lock, 4) \ +X(a, STATIC, SINGULAR, BOOL, settings_lock, 5) \ +X(a, STATIC, SINGULAR, UINT32, pin_code, 6) \ +X(a, STATIC, SINGULAR, UENUM, theme, 7) \ +X(a, STATIC, SINGULAR, BOOL, alert_enabled, 8) \ +X(a, STATIC, SINGULAR, BOOL, banner_enabled, 9) \ +X(a, STATIC, SINGULAR, UINT32, ring_tone_id, 10) \ +X(a, STATIC, SINGULAR, UENUM, language, 11) \ +X(a, STATIC, OPTIONAL, MESSAGE, node_filter, 12) \ +X(a, STATIC, OPTIONAL, MESSAGE, node_highlight, 13) +#define meshtastic_DeviceUIConfig_CALLBACK NULL +#define meshtastic_DeviceUIConfig_DEFAULT NULL +#define meshtastic_DeviceUIConfig_node_filter_MSGTYPE meshtastic_NodeFilter +#define meshtastic_DeviceUIConfig_node_highlight_MSGTYPE meshtastic_NodeHighlight + +#define meshtastic_NodeFilter_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, unknown_switch, 1) \ +X(a, STATIC, SINGULAR, BOOL, offline_switch, 2) \ +X(a, STATIC, SINGULAR, BOOL, public_key_switch, 3) \ +X(a, STATIC, SINGULAR, INT32, hops_away, 4) \ +X(a, STATIC, SINGULAR, BOOL, position_switch, 5) \ +X(a, STATIC, SINGULAR, STRING, node_name, 6) +#define meshtastic_NodeFilter_CALLBACK NULL +#define meshtastic_NodeFilter_DEFAULT NULL + +#define meshtastic_NodeHighlight_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, chat_switch, 1) \ +X(a, STATIC, SINGULAR, BOOL, position_switch, 2) \ +X(a, STATIC, SINGULAR, BOOL, telemetry_switch, 3) \ +X(a, STATIC, SINGULAR, BOOL, iaq_switch, 4) \ +X(a, STATIC, SINGULAR, STRING, node_name, 5) +#define meshtastic_NodeHighlight_CALLBACK NULL +#define meshtastic_NodeHighlight_DEFAULT NULL + +extern const pb_msgdesc_t meshtastic_DeviceUIConfig_msg; +extern const pb_msgdesc_t meshtastic_NodeFilter_msg; +extern const pb_msgdesc_t meshtastic_NodeHighlight_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define meshtastic_DeviceUIConfig_fields &meshtastic_DeviceUIConfig_msg +#define meshtastic_NodeFilter_fields &meshtastic_NodeFilter_msg +#define meshtastic_NodeHighlight_fields &meshtastic_NodeHighlight_msg + +/* Maximum encoded size of messages (where known) */ +#define MESHTASTIC_MESHTASTIC_DEVICE_UI_PB_H_MAX_SIZE meshtastic_DeviceUIConfig_size +#define meshtastic_DeviceUIConfig_size 99 +#define meshtastic_NodeFilter_size 36 +#define meshtastic_NodeHighlight_size 25 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.cpp b/src/mesh/generated/meshtastic/deviceonly.pb.cpp new file mode 100644 index 0000000..92853f0 --- /dev/null +++ b/src/mesh/generated/meshtastic/deviceonly.pb.cpp @@ -0,0 +1,24 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9 */ + +#include "meshtastic/deviceonly.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(meshtastic_PositionLite, meshtastic_PositionLite, AUTO) + + +PB_BIND(meshtastic_UserLite, meshtastic_UserLite, AUTO) + + +PB_BIND(meshtastic_NodeInfoLite, meshtastic_NodeInfoLite, AUTO) + + +PB_BIND(meshtastic_DeviceState, meshtastic_DeviceState, 2) + + +PB_BIND(meshtastic_ChannelFile, meshtastic_ChannelFile, 2) + + + diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h new file mode 100644 index 0000000..a90e722 --- /dev/null +++ b/src/mesh/generated/meshtastic/deviceonly.pb.h @@ -0,0 +1,290 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9 */ + +#ifndef PB_MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_INCLUDED +#define PB_MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_INCLUDED +#include +#include +#include "meshtastic/channel.pb.h" +#include "meshtastic/mesh.pb.h" +#include "meshtastic/telemetry.pb.h" +#include "meshtastic/config.pb.h" + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Struct definitions */ +/* Position with static location information only for NodeDBLite */ +typedef struct _meshtastic_PositionLite { + /* The new preferred location encoding, multiply by 1e-7 to get degrees + in floating point */ + int32_t latitude_i; + /* TODO: REPLACE */ + int32_t longitude_i; + /* In meters above MSL (but see issue #359) */ + int32_t altitude; + /* This is usually not sent over the mesh (to save space), but it is sent + from the phone so that the local device can set its RTC If it is sent over + the mesh (because there are devices on the mesh without GPS), it will only + be sent by devices which has a hardware GPS clock. + seconds since 1970 */ + uint32_t time; + /* TODO: REPLACE */ + meshtastic_Position_LocSource location_source; +} meshtastic_PositionLite; + +typedef PB_BYTES_ARRAY_T(32) meshtastic_UserLite_public_key_t; +typedef struct _meshtastic_UserLite { + /* This is the addr of the radio. */ + pb_byte_t macaddr[6]; + /* A full name for this user, i.e. "Kevin Hester" */ + char long_name[40]; + /* A VERY short name, ideally two characters. + Suitable for a tiny OLED screen */ + char short_name[5]; + /* TBEAM, HELTEC, etc... + Starting in 1.2.11 moved to hw_model enum in the NodeInfo object. + Apps will still need the string here for older builds + (so OTA update can find the right image), but if the enum is available it will be used instead. */ + meshtastic_HardwareModel hw_model; + /* In some regions Ham radio operators have different bandwidth limitations than others. + If this user is a licensed operator, set this flag. + Also, "long_name" should be their licence number. */ + bool is_licensed; + /* Indicates that the user's role in the mesh */ + meshtastic_Config_DeviceConfig_Role role; + /* The public key of the user's device. + This is sent out to other nodes on the mesh to allow them to compute a shared secret key. */ + meshtastic_UserLite_public_key_t public_key; +} meshtastic_UserLite; + +typedef struct _meshtastic_NodeInfoLite { + /* The node number */ + uint32_t num; + /* The user info for this node */ + bool has_user; + meshtastic_UserLite user; + /* This position data. Note: before 1.2.14 we would also store the last time we've heard from this node in position.time, that is no longer true. + Position.time now indicates the last time we received a POSITION from that node. */ + bool has_position; + meshtastic_PositionLite position; + /* Returns the Signal-to-noise ratio (SNR) of the last received message, + as measured by the receiver. Return SNR of the last received message in dB */ + float snr; + /* Set to indicate the last time we received a packet from this node */ + uint32_t last_heard; + /* The latest device metrics for the node. */ + bool has_device_metrics; + meshtastic_DeviceMetrics device_metrics; + /* local channel index we heard that node on. Only populated if its not the default channel. */ + uint8_t channel; + /* True if we witnessed the node over MQTT instead of LoRA transport */ + bool via_mqtt; + /* Number of hops away from us this node is (0 if adjacent) */ + bool has_hops_away; + uint8_t hops_away; + /* True if node is in our favorites list + Persists between NodeDB internal clean ups */ + bool is_favorite; +} meshtastic_NodeInfoLite; + +/* This message is never sent over the wire, but it is used for serializing DB + state to flash in the device code + FIXME, since we write this each time we enter deep sleep (and have infinite + flash) it would be better to use some sort of append only data structure for + the receive queue and use the preferences store for the other stuff */ +typedef struct _meshtastic_DeviceState { + /* Read only settings/info about this node */ + bool has_my_node; + meshtastic_MyNodeInfo my_node; + /* My owner info */ + bool has_owner; + meshtastic_User owner; + /* Received packets saved for delivery to the phone */ + pb_size_t receive_queue_count; + meshtastic_MeshPacket receive_queue[1]; + /* We keep the last received text message (only) stored in the device flash, + so we can show it on the screen. + Might be null */ + bool has_rx_text_message; + meshtastic_MeshPacket rx_text_message; + /* A version integer used to invalidate old save files when we make + incompatible changes This integer is set at build time and is private to + NodeDB.cpp in the device code. */ + uint32_t version; + /* Used only during development. + Indicates developer is testing and changes should never be saved to flash. + Deprecated in 2.3.1 */ + bool no_save; + /* Some GPS receivers seem to have bogus settings from the factory, so we always do one factory reset. */ + bool did_gps_reset; + /* We keep the last received waypoint stored in the device flash, + so we can show it on the screen. + Might be null */ + bool has_rx_waypoint; + meshtastic_MeshPacket rx_waypoint; + /* The mesh's nodes with their available gpio pins for RemoteHardware module */ + pb_size_t node_remote_hardware_pins_count; + meshtastic_NodeRemoteHardwarePin node_remote_hardware_pins[12]; + /* New lite version of NodeDB to decrease memory footprint */ + std::vector node_db_lite; +} meshtastic_DeviceState; + +/* The on-disk saved channels */ +typedef struct _meshtastic_ChannelFile { + /* The channels our node knows about */ + pb_size_t channels_count; + meshtastic_Channel channels[8]; + /* A version integer used to invalidate old save files when we make + incompatible changes This integer is set at build time and is private to + NodeDB.cpp in the device code. */ + uint32_t version; +} meshtastic_ChannelFile; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initializer values for message structs */ +#define meshtastic_PositionLite_init_default {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN} +#define meshtastic_UserLite_init_default {{0}, "", "", _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}} +#define meshtastic_NodeInfoLite_init_default {0, false, meshtastic_UserLite_init_default, false, meshtastic_PositionLite_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0} +#define meshtastic_DeviceState_init_default {false, meshtastic_MyNodeInfo_init_default, false, meshtastic_User_init_default, 0, {meshtastic_MeshPacket_init_default}, false, meshtastic_MeshPacket_init_default, 0, 0, 0, false, meshtastic_MeshPacket_init_default, 0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}, {0}} +#define meshtastic_ChannelFile_init_default {0, {meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default}, 0} +#define meshtastic_PositionLite_init_zero {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN} +#define meshtastic_UserLite_init_zero {{0}, "", "", _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}} +#define meshtastic_NodeInfoLite_init_zero {0, false, meshtastic_UserLite_init_zero, false, meshtastic_PositionLite_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0} +#define meshtastic_DeviceState_init_zero {false, meshtastic_MyNodeInfo_init_zero, false, meshtastic_User_init_zero, 0, {meshtastic_MeshPacket_init_zero}, false, meshtastic_MeshPacket_init_zero, 0, 0, 0, false, meshtastic_MeshPacket_init_zero, 0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}, {0}} +#define meshtastic_ChannelFile_init_zero {0, {meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero}, 0} + +/* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_PositionLite_latitude_i_tag 1 +#define meshtastic_PositionLite_longitude_i_tag 2 +#define meshtastic_PositionLite_altitude_tag 3 +#define meshtastic_PositionLite_time_tag 4 +#define meshtastic_PositionLite_location_source_tag 5 +#define meshtastic_UserLite_macaddr_tag 1 +#define meshtastic_UserLite_long_name_tag 2 +#define meshtastic_UserLite_short_name_tag 3 +#define meshtastic_UserLite_hw_model_tag 4 +#define meshtastic_UserLite_is_licensed_tag 5 +#define meshtastic_UserLite_role_tag 6 +#define meshtastic_UserLite_public_key_tag 7 +#define meshtastic_NodeInfoLite_num_tag 1 +#define meshtastic_NodeInfoLite_user_tag 2 +#define meshtastic_NodeInfoLite_position_tag 3 +#define meshtastic_NodeInfoLite_snr_tag 4 +#define meshtastic_NodeInfoLite_last_heard_tag 5 +#define meshtastic_NodeInfoLite_device_metrics_tag 6 +#define meshtastic_NodeInfoLite_channel_tag 7 +#define meshtastic_NodeInfoLite_via_mqtt_tag 8 +#define meshtastic_NodeInfoLite_hops_away_tag 9 +#define meshtastic_NodeInfoLite_is_favorite_tag 10 +#define meshtastic_DeviceState_my_node_tag 2 +#define meshtastic_DeviceState_owner_tag 3 +#define meshtastic_DeviceState_receive_queue_tag 5 +#define meshtastic_DeviceState_rx_text_message_tag 7 +#define meshtastic_DeviceState_version_tag 8 +#define meshtastic_DeviceState_no_save_tag 9 +#define meshtastic_DeviceState_did_gps_reset_tag 11 +#define meshtastic_DeviceState_rx_waypoint_tag 12 +#define meshtastic_DeviceState_node_remote_hardware_pins_tag 13 +#define meshtastic_DeviceState_node_db_lite_tag 14 +#define meshtastic_ChannelFile_channels_tag 1 +#define meshtastic_ChannelFile_version_tag 2 + +/* Struct field encoding specification for nanopb */ +#define meshtastic_PositionLite_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, SFIXED32, latitude_i, 1) \ +X(a, STATIC, SINGULAR, SFIXED32, longitude_i, 2) \ +X(a, STATIC, SINGULAR, INT32, altitude, 3) \ +X(a, STATIC, SINGULAR, FIXED32, time, 4) \ +X(a, STATIC, SINGULAR, UENUM, location_source, 5) +#define meshtastic_PositionLite_CALLBACK NULL +#define meshtastic_PositionLite_DEFAULT NULL + +#define meshtastic_UserLite_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, FIXED_LENGTH_BYTES, macaddr, 1) \ +X(a, STATIC, SINGULAR, STRING, long_name, 2) \ +X(a, STATIC, SINGULAR, STRING, short_name, 3) \ +X(a, STATIC, SINGULAR, UENUM, hw_model, 4) \ +X(a, STATIC, SINGULAR, BOOL, is_licensed, 5) \ +X(a, STATIC, SINGULAR, UENUM, role, 6) \ +X(a, STATIC, SINGULAR, BYTES, public_key, 7) +#define meshtastic_UserLite_CALLBACK NULL +#define meshtastic_UserLite_DEFAULT NULL + +#define meshtastic_NodeInfoLite_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, num, 1) \ +X(a, STATIC, OPTIONAL, MESSAGE, user, 2) \ +X(a, STATIC, OPTIONAL, MESSAGE, position, 3) \ +X(a, STATIC, SINGULAR, FLOAT, snr, 4) \ +X(a, STATIC, SINGULAR, FIXED32, last_heard, 5) \ +X(a, STATIC, OPTIONAL, MESSAGE, device_metrics, 6) \ +X(a, STATIC, SINGULAR, UINT32, channel, 7) \ +X(a, STATIC, SINGULAR, BOOL, via_mqtt, 8) \ +X(a, STATIC, OPTIONAL, UINT32, hops_away, 9) \ +X(a, STATIC, SINGULAR, BOOL, is_favorite, 10) +#define meshtastic_NodeInfoLite_CALLBACK NULL +#define meshtastic_NodeInfoLite_DEFAULT NULL +#define meshtastic_NodeInfoLite_user_MSGTYPE meshtastic_UserLite +#define meshtastic_NodeInfoLite_position_MSGTYPE meshtastic_PositionLite +#define meshtastic_NodeInfoLite_device_metrics_MSGTYPE meshtastic_DeviceMetrics + +#define meshtastic_DeviceState_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, MESSAGE, my_node, 2) \ +X(a, STATIC, OPTIONAL, MESSAGE, owner, 3) \ +X(a, STATIC, REPEATED, MESSAGE, receive_queue, 5) \ +X(a, STATIC, OPTIONAL, MESSAGE, rx_text_message, 7) \ +X(a, STATIC, SINGULAR, UINT32, version, 8) \ +X(a, STATIC, SINGULAR, BOOL, no_save, 9) \ +X(a, STATIC, SINGULAR, BOOL, did_gps_reset, 11) \ +X(a, STATIC, OPTIONAL, MESSAGE, rx_waypoint, 12) \ +X(a, STATIC, REPEATED, MESSAGE, node_remote_hardware_pins, 13) \ +X(a, CALLBACK, REPEATED, MESSAGE, node_db_lite, 14) +extern bool meshtastic_DeviceState_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field); +#define meshtastic_DeviceState_CALLBACK meshtastic_DeviceState_callback +#define meshtastic_DeviceState_DEFAULT NULL +#define meshtastic_DeviceState_my_node_MSGTYPE meshtastic_MyNodeInfo +#define meshtastic_DeviceState_owner_MSGTYPE meshtastic_User +#define meshtastic_DeviceState_receive_queue_MSGTYPE meshtastic_MeshPacket +#define meshtastic_DeviceState_rx_text_message_MSGTYPE meshtastic_MeshPacket +#define meshtastic_DeviceState_rx_waypoint_MSGTYPE meshtastic_MeshPacket +#define meshtastic_DeviceState_node_remote_hardware_pins_MSGTYPE meshtastic_NodeRemoteHardwarePin +#define meshtastic_DeviceState_node_db_lite_MSGTYPE meshtastic_NodeInfoLite + +#define meshtastic_ChannelFile_FIELDLIST(X, a) \ +X(a, STATIC, REPEATED, MESSAGE, channels, 1) \ +X(a, STATIC, SINGULAR, UINT32, version, 2) +#define meshtastic_ChannelFile_CALLBACK NULL +#define meshtastic_ChannelFile_DEFAULT NULL +#define meshtastic_ChannelFile_channels_MSGTYPE meshtastic_Channel + +extern const pb_msgdesc_t meshtastic_PositionLite_msg; +extern const pb_msgdesc_t meshtastic_UserLite_msg; +extern const pb_msgdesc_t meshtastic_NodeInfoLite_msg; +extern const pb_msgdesc_t meshtastic_DeviceState_msg; +extern const pb_msgdesc_t meshtastic_ChannelFile_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define meshtastic_PositionLite_fields &meshtastic_PositionLite_msg +#define meshtastic_UserLite_fields &meshtastic_UserLite_msg +#define meshtastic_NodeInfoLite_fields &meshtastic_NodeInfoLite_msg +#define meshtastic_DeviceState_fields &meshtastic_DeviceState_msg +#define meshtastic_ChannelFile_fields &meshtastic_ChannelFile_msg + +/* Maximum encoded size of messages (where known) */ +/* meshtastic_DeviceState_size depends on runtime parameters */ +#define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_ChannelFile_size +#define meshtastic_ChannelFile_size 718 +#define meshtastic_NodeInfoLite_size 183 +#define meshtastic_PositionLite_size 28 +#define meshtastic_UserLite_size 96 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/mesh/generated/meshtastic/localonly.pb.cpp b/src/mesh/generated/meshtastic/localonly.pb.cpp new file mode 100644 index 0000000..0a752a5 --- /dev/null +++ b/src/mesh/generated/meshtastic/localonly.pb.cpp @@ -0,0 +1,15 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9 */ + +#include "meshtastic/localonly.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(meshtastic_LocalConfig, meshtastic_LocalConfig, 2) + + +PB_BIND(meshtastic_LocalModuleConfig, meshtastic_LocalModuleConfig, 2) + + + diff --git a/src/mesh/generated/meshtastic/localonly.pb.h b/src/mesh/generated/meshtastic/localonly.pb.h new file mode 100644 index 0000000..6409aef --- /dev/null +++ b/src/mesh/generated/meshtastic/localonly.pb.h @@ -0,0 +1,197 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9 */ + +#ifndef PB_MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_INCLUDED +#define PB_MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_INCLUDED +#include +#include "meshtastic/config.pb.h" +#include "meshtastic/module_config.pb.h" + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Struct definitions */ +typedef struct _meshtastic_LocalConfig { + /* The part of the config that is specific to the Device */ + bool has_device; + meshtastic_Config_DeviceConfig device; + /* The part of the config that is specific to the GPS Position */ + bool has_position; + meshtastic_Config_PositionConfig position; + /* The part of the config that is specific to the Power settings */ + bool has_power; + meshtastic_Config_PowerConfig power; + /* The part of the config that is specific to the Wifi Settings */ + bool has_network; + meshtastic_Config_NetworkConfig network; + /* The part of the config that is specific to the Display */ + bool has_display; + meshtastic_Config_DisplayConfig display; + /* The part of the config that is specific to the Lora Radio */ + bool has_lora; + meshtastic_Config_LoRaConfig lora; + /* The part of the config that is specific to the Bluetooth settings */ + bool has_bluetooth; + meshtastic_Config_BluetoothConfig bluetooth; + /* A version integer used to invalidate old save files when we make + incompatible changes This integer is set at build time and is private to + NodeDB.cpp in the device code. */ + uint32_t version; + /* The part of the config that is specific to Security settings */ + bool has_security; + meshtastic_Config_SecurityConfig security; +} meshtastic_LocalConfig; + +typedef struct _meshtastic_LocalModuleConfig { + /* The part of the config that is specific to the MQTT module */ + bool has_mqtt; + meshtastic_ModuleConfig_MQTTConfig mqtt; + /* The part of the config that is specific to the Serial module */ + bool has_serial; + meshtastic_ModuleConfig_SerialConfig serial; + /* The part of the config that is specific to the ExternalNotification module */ + bool has_external_notification; + meshtastic_ModuleConfig_ExternalNotificationConfig external_notification; + /* The part of the config that is specific to the Store & Forward module */ + bool has_store_forward; + meshtastic_ModuleConfig_StoreForwardConfig store_forward; + /* The part of the config that is specific to the RangeTest module */ + bool has_range_test; + meshtastic_ModuleConfig_RangeTestConfig range_test; + /* The part of the config that is specific to the Telemetry module */ + bool has_telemetry; + meshtastic_ModuleConfig_TelemetryConfig telemetry; + /* The part of the config that is specific to the Canned Message module */ + bool has_canned_message; + meshtastic_ModuleConfig_CannedMessageConfig canned_message; + /* A version integer used to invalidate old save files when we make + incompatible changes This integer is set at build time and is private to + NodeDB.cpp in the device code. */ + uint32_t version; + /* The part of the config that is specific to the Audio module */ + bool has_audio; + meshtastic_ModuleConfig_AudioConfig audio; + /* The part of the config that is specific to the Remote Hardware module */ + bool has_remote_hardware; + meshtastic_ModuleConfig_RemoteHardwareConfig remote_hardware; + /* The part of the config that is specific to the Neighbor Info module */ + bool has_neighbor_info; + meshtastic_ModuleConfig_NeighborInfoConfig neighbor_info; + /* The part of the config that is specific to the Ambient Lighting module */ + bool has_ambient_lighting; + meshtastic_ModuleConfig_AmbientLightingConfig ambient_lighting; + /* The part of the config that is specific to the Detection Sensor module */ + bool has_detection_sensor; + meshtastic_ModuleConfig_DetectionSensorConfig detection_sensor; + /* Paxcounter Config */ + bool has_paxcounter; + meshtastic_ModuleConfig_PaxcounterConfig paxcounter; +} meshtastic_LocalModuleConfig; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initializer values for message structs */ +#define meshtastic_LocalConfig_init_default {false, meshtastic_Config_DeviceConfig_init_default, false, meshtastic_Config_PositionConfig_init_default, false, meshtastic_Config_PowerConfig_init_default, false, meshtastic_Config_NetworkConfig_init_default, false, meshtastic_Config_DisplayConfig_init_default, false, meshtastic_Config_LoRaConfig_init_default, false, meshtastic_Config_BluetoothConfig_init_default, 0, false, meshtastic_Config_SecurityConfig_init_default} +#define meshtastic_LocalModuleConfig_init_default {false, meshtastic_ModuleConfig_MQTTConfig_init_default, false, meshtastic_ModuleConfig_SerialConfig_init_default, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_default, false, meshtastic_ModuleConfig_StoreForwardConfig_init_default, false, meshtastic_ModuleConfig_RangeTestConfig_init_default, false, meshtastic_ModuleConfig_TelemetryConfig_init_default, false, meshtastic_ModuleConfig_CannedMessageConfig_init_default, 0, false, meshtastic_ModuleConfig_AudioConfig_init_default, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_default, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_default, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_default, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_default, false, meshtastic_ModuleConfig_PaxcounterConfig_init_default} +#define meshtastic_LocalConfig_init_zero {false, meshtastic_Config_DeviceConfig_init_zero, false, meshtastic_Config_PositionConfig_init_zero, false, meshtastic_Config_PowerConfig_init_zero, false, meshtastic_Config_NetworkConfig_init_zero, false, meshtastic_Config_DisplayConfig_init_zero, false, meshtastic_Config_LoRaConfig_init_zero, false, meshtastic_Config_BluetoothConfig_init_zero, 0, false, meshtastic_Config_SecurityConfig_init_zero} +#define meshtastic_LocalModuleConfig_init_zero {false, meshtastic_ModuleConfig_MQTTConfig_init_zero, false, meshtastic_ModuleConfig_SerialConfig_init_zero, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero, false, meshtastic_ModuleConfig_StoreForwardConfig_init_zero, false, meshtastic_ModuleConfig_RangeTestConfig_init_zero, false, meshtastic_ModuleConfig_TelemetryConfig_init_zero, false, meshtastic_ModuleConfig_CannedMessageConfig_init_zero, 0, false, meshtastic_ModuleConfig_AudioConfig_init_zero, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_zero, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_zero, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_zero, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_zero, false, meshtastic_ModuleConfig_PaxcounterConfig_init_zero} + +/* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_LocalConfig_device_tag 1 +#define meshtastic_LocalConfig_position_tag 2 +#define meshtastic_LocalConfig_power_tag 3 +#define meshtastic_LocalConfig_network_tag 4 +#define meshtastic_LocalConfig_display_tag 5 +#define meshtastic_LocalConfig_lora_tag 6 +#define meshtastic_LocalConfig_bluetooth_tag 7 +#define meshtastic_LocalConfig_version_tag 8 +#define meshtastic_LocalConfig_security_tag 9 +#define meshtastic_LocalModuleConfig_mqtt_tag 1 +#define meshtastic_LocalModuleConfig_serial_tag 2 +#define meshtastic_LocalModuleConfig_external_notification_tag 3 +#define meshtastic_LocalModuleConfig_store_forward_tag 4 +#define meshtastic_LocalModuleConfig_range_test_tag 5 +#define meshtastic_LocalModuleConfig_telemetry_tag 6 +#define meshtastic_LocalModuleConfig_canned_message_tag 7 +#define meshtastic_LocalModuleConfig_version_tag 8 +#define meshtastic_LocalModuleConfig_audio_tag 9 +#define meshtastic_LocalModuleConfig_remote_hardware_tag 10 +#define meshtastic_LocalModuleConfig_neighbor_info_tag 11 +#define meshtastic_LocalModuleConfig_ambient_lighting_tag 12 +#define meshtastic_LocalModuleConfig_detection_sensor_tag 13 +#define meshtastic_LocalModuleConfig_paxcounter_tag 14 + +/* Struct field encoding specification for nanopb */ +#define meshtastic_LocalConfig_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, MESSAGE, device, 1) \ +X(a, STATIC, OPTIONAL, MESSAGE, position, 2) \ +X(a, STATIC, OPTIONAL, MESSAGE, power, 3) \ +X(a, STATIC, OPTIONAL, MESSAGE, network, 4) \ +X(a, STATIC, OPTIONAL, MESSAGE, display, 5) \ +X(a, STATIC, OPTIONAL, MESSAGE, lora, 6) \ +X(a, STATIC, OPTIONAL, MESSAGE, bluetooth, 7) \ +X(a, STATIC, SINGULAR, UINT32, version, 8) \ +X(a, STATIC, OPTIONAL, MESSAGE, security, 9) +#define meshtastic_LocalConfig_CALLBACK NULL +#define meshtastic_LocalConfig_DEFAULT NULL +#define meshtastic_LocalConfig_device_MSGTYPE meshtastic_Config_DeviceConfig +#define meshtastic_LocalConfig_position_MSGTYPE meshtastic_Config_PositionConfig +#define meshtastic_LocalConfig_power_MSGTYPE meshtastic_Config_PowerConfig +#define meshtastic_LocalConfig_network_MSGTYPE meshtastic_Config_NetworkConfig +#define meshtastic_LocalConfig_display_MSGTYPE meshtastic_Config_DisplayConfig +#define meshtastic_LocalConfig_lora_MSGTYPE meshtastic_Config_LoRaConfig +#define meshtastic_LocalConfig_bluetooth_MSGTYPE meshtastic_Config_BluetoothConfig +#define meshtastic_LocalConfig_security_MSGTYPE meshtastic_Config_SecurityConfig + +#define meshtastic_LocalModuleConfig_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, MESSAGE, mqtt, 1) \ +X(a, STATIC, OPTIONAL, MESSAGE, serial, 2) \ +X(a, STATIC, OPTIONAL, MESSAGE, external_notification, 3) \ +X(a, STATIC, OPTIONAL, MESSAGE, store_forward, 4) \ +X(a, STATIC, OPTIONAL, MESSAGE, range_test, 5) \ +X(a, STATIC, OPTIONAL, MESSAGE, telemetry, 6) \ +X(a, STATIC, OPTIONAL, MESSAGE, canned_message, 7) \ +X(a, STATIC, SINGULAR, UINT32, version, 8) \ +X(a, STATIC, OPTIONAL, MESSAGE, audio, 9) \ +X(a, STATIC, OPTIONAL, MESSAGE, remote_hardware, 10) \ +X(a, STATIC, OPTIONAL, MESSAGE, neighbor_info, 11) \ +X(a, STATIC, OPTIONAL, MESSAGE, ambient_lighting, 12) \ +X(a, STATIC, OPTIONAL, MESSAGE, detection_sensor, 13) \ +X(a, STATIC, OPTIONAL, MESSAGE, paxcounter, 14) +#define meshtastic_LocalModuleConfig_CALLBACK NULL +#define meshtastic_LocalModuleConfig_DEFAULT NULL +#define meshtastic_LocalModuleConfig_mqtt_MSGTYPE meshtastic_ModuleConfig_MQTTConfig +#define meshtastic_LocalModuleConfig_serial_MSGTYPE meshtastic_ModuleConfig_SerialConfig +#define meshtastic_LocalModuleConfig_external_notification_MSGTYPE meshtastic_ModuleConfig_ExternalNotificationConfig +#define meshtastic_LocalModuleConfig_store_forward_MSGTYPE meshtastic_ModuleConfig_StoreForwardConfig +#define meshtastic_LocalModuleConfig_range_test_MSGTYPE meshtastic_ModuleConfig_RangeTestConfig +#define meshtastic_LocalModuleConfig_telemetry_MSGTYPE meshtastic_ModuleConfig_TelemetryConfig +#define meshtastic_LocalModuleConfig_canned_message_MSGTYPE meshtastic_ModuleConfig_CannedMessageConfig +#define meshtastic_LocalModuleConfig_audio_MSGTYPE meshtastic_ModuleConfig_AudioConfig +#define meshtastic_LocalModuleConfig_remote_hardware_MSGTYPE meshtastic_ModuleConfig_RemoteHardwareConfig +#define meshtastic_LocalModuleConfig_neighbor_info_MSGTYPE meshtastic_ModuleConfig_NeighborInfoConfig +#define meshtastic_LocalModuleConfig_ambient_lighting_MSGTYPE meshtastic_ModuleConfig_AmbientLightingConfig +#define meshtastic_LocalModuleConfig_detection_sensor_MSGTYPE meshtastic_ModuleConfig_DetectionSensorConfig +#define meshtastic_LocalModuleConfig_paxcounter_MSGTYPE meshtastic_ModuleConfig_PaxcounterConfig + +extern const pb_msgdesc_t meshtastic_LocalConfig_msg; +extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define meshtastic_LocalConfig_fields &meshtastic_LocalConfig_msg +#define meshtastic_LocalModuleConfig_fields &meshtastic_LocalModuleConfig_msg + +/* Maximum encoded size of messages (where known) */ +#define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalConfig_size +#define meshtastic_LocalConfig_size 735 +#define meshtastic_LocalModuleConfig_size 697 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/mesh/generated/meshtastic/mesh.pb.cpp b/src/mesh/generated/meshtastic/mesh.pb.cpp new file mode 100644 index 0000000..a0c1e2e --- /dev/null +++ b/src/mesh/generated/meshtastic/mesh.pb.cpp @@ -0,0 +1,102 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9 */ + +#include "meshtastic/mesh.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(meshtastic_Position, meshtastic_Position, AUTO) + + +PB_BIND(meshtastic_User, meshtastic_User, AUTO) + + +PB_BIND(meshtastic_RouteDiscovery, meshtastic_RouteDiscovery, AUTO) + + +PB_BIND(meshtastic_Routing, meshtastic_Routing, AUTO) + + +PB_BIND(meshtastic_Data, meshtastic_Data, 2) + + +PB_BIND(meshtastic_Waypoint, meshtastic_Waypoint, AUTO) + + +PB_BIND(meshtastic_MqttClientProxyMessage, meshtastic_MqttClientProxyMessage, 2) + + +PB_BIND(meshtastic_MeshPacket, meshtastic_MeshPacket, 2) + + +PB_BIND(meshtastic_NodeInfo, meshtastic_NodeInfo, 2) + + +PB_BIND(meshtastic_MyNodeInfo, meshtastic_MyNodeInfo, AUTO) + + +PB_BIND(meshtastic_LogRecord, meshtastic_LogRecord, 2) + + +PB_BIND(meshtastic_QueueStatus, meshtastic_QueueStatus, AUTO) + + +PB_BIND(meshtastic_FromRadio, meshtastic_FromRadio, 2) + + +PB_BIND(meshtastic_ClientNotification, meshtastic_ClientNotification, 2) + + +PB_BIND(meshtastic_FileInfo, meshtastic_FileInfo, AUTO) + + +PB_BIND(meshtastic_ToRadio, meshtastic_ToRadio, 2) + + +PB_BIND(meshtastic_Compressed, meshtastic_Compressed, AUTO) + + +PB_BIND(meshtastic_NeighborInfo, meshtastic_NeighborInfo, AUTO) + + +PB_BIND(meshtastic_Neighbor, meshtastic_Neighbor, AUTO) + + +PB_BIND(meshtastic_DeviceMetadata, meshtastic_DeviceMetadata, AUTO) + + +PB_BIND(meshtastic_Heartbeat, meshtastic_Heartbeat, AUTO) + + +PB_BIND(meshtastic_NodeRemoteHardwarePin, meshtastic_NodeRemoteHardwarePin, AUTO) + + +PB_BIND(meshtastic_ChunkedPayload, meshtastic_ChunkedPayload, AUTO) + + +PB_BIND(meshtastic_resend_chunks, meshtastic_resend_chunks, AUTO) + + +PB_BIND(meshtastic_ChunkedPayloadResponse, meshtastic_ChunkedPayloadResponse, AUTO) + + + + + + + + + + + + + + + + + + + + + diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h new file mode 100644 index 0000000..1725435 --- /dev/null +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -0,0 +1,1694 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9 */ + +#ifndef PB_MESHTASTIC_MESHTASTIC_MESH_PB_H_INCLUDED +#define PB_MESHTASTIC_MESHTASTIC_MESH_PB_H_INCLUDED +#include +#include "meshtastic/channel.pb.h" +#include "meshtastic/config.pb.h" +#include "meshtastic/module_config.pb.h" +#include "meshtastic/portnums.pb.h" +#include "meshtastic/telemetry.pb.h" +#include "meshtastic/xmodem.pb.h" +#include "meshtastic/device_ui.pb.h" + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Enum definitions */ +/* Note: these enum names must EXACTLY match the string used in the device + bin/build-all.sh script. + Because they will be used to find firmware filenames in the android app for OTA updates. + To match the old style filenames, _ is converted to -, p is converted to . */ +typedef enum _meshtastic_HardwareModel { + /* TODO: REPLACE */ + meshtastic_HardwareModel_UNSET = 0, + /* TODO: REPLACE */ + meshtastic_HardwareModel_TLORA_V2 = 1, + /* TODO: REPLACE */ + meshtastic_HardwareModel_TLORA_V1 = 2, + /* TODO: REPLACE */ + meshtastic_HardwareModel_TLORA_V2_1_1P6 = 3, + /* TODO: REPLACE */ + meshtastic_HardwareModel_TBEAM = 4, + /* The original heltec WiFi_Lora_32_V2, which had battery voltage sensing hooked to GPIO 13 + (see HELTEC_V2 for the new version). */ + meshtastic_HardwareModel_HELTEC_V2_0 = 5, + /* TODO: REPLACE */ + meshtastic_HardwareModel_TBEAM_V0P7 = 6, + /* TODO: REPLACE */ + meshtastic_HardwareModel_T_ECHO = 7, + /* TODO: REPLACE */ + meshtastic_HardwareModel_TLORA_V1_1P3 = 8, + /* TODO: REPLACE */ + meshtastic_HardwareModel_RAK4631 = 9, + /* The new version of the heltec WiFi_Lora_32_V2 board that has battery sensing hooked to GPIO 37. + Sadly they did not update anything on the silkscreen to identify this board */ + meshtastic_HardwareModel_HELTEC_V2_1 = 10, + /* Ancient heltec WiFi_Lora_32 board */ + meshtastic_HardwareModel_HELTEC_V1 = 11, + /* New T-BEAM with ESP32-S3 CPU */ + meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE = 12, + /* RAK WisBlock ESP32 core: https://docs.rakwireless.com/Product-Categories/WisBlock/RAK11200/Overview/ */ + meshtastic_HardwareModel_RAK11200 = 13, + /* B&Q Consulting Nano Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:nano */ + meshtastic_HardwareModel_NANO_G1 = 14, + /* TODO: REPLACE */ + meshtastic_HardwareModel_TLORA_V2_1_1P8 = 15, + /* TODO: REPLACE */ + meshtastic_HardwareModel_TLORA_T3_S3 = 16, + /* B&Q Consulting Nano G1 Explorer: https://wiki.uniteng.com/en/meshtastic/nano-g1-explorer */ + meshtastic_HardwareModel_NANO_G1_EXPLORER = 17, + /* B&Q Consulting Nano G2 Ultra: https://wiki.uniteng.com/en/meshtastic/nano-g2-ultra */ + meshtastic_HardwareModel_NANO_G2_ULTRA = 18, + /* LoRAType device: https://loratype.org/ */ + meshtastic_HardwareModel_LORA_TYPE = 19, + /* wiphone https://www.wiphone.io/ */ + meshtastic_HardwareModel_WIPHONE = 20, + /* WIO Tracker WM1110 family from Seeed Studio. Includes wio-1110-tracker and wio-1110-sdk */ + meshtastic_HardwareModel_WIO_WM1110 = 21, + /* RAK2560 Solar base station based on RAK4630 */ + meshtastic_HardwareModel_RAK2560 = 22, + /* Heltec HRU-3601: https://heltec.org/project/hru-3601/ */ + meshtastic_HardwareModel_HELTEC_HRU_3601 = 23, + /* Heltec Wireless Bridge */ + meshtastic_HardwareModel_HELTEC_WIRELESS_BRIDGE = 24, + /* B&Q Consulting Station Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:station */ + meshtastic_HardwareModel_STATION_G1 = 25, + /* RAK11310 (RP2040 + SX1262) */ + meshtastic_HardwareModel_RAK11310 = 26, + /* Makerfabs SenseLoRA Receiver (RP2040 + RFM96) */ + meshtastic_HardwareModel_SENSELORA_RP2040 = 27, + /* Makerfabs SenseLoRA Industrial Monitor (ESP32-S3 + RFM96) */ + meshtastic_HardwareModel_SENSELORA_S3 = 28, + /* Canary Radio Company - CanaryOne: https://canaryradio.io/products/canaryone */ + meshtastic_HardwareModel_CANARYONE = 29, + /* Waveshare RP2040 LoRa - https://www.waveshare.com/rp2040-lora.htm */ + meshtastic_HardwareModel_RP2040_LORA = 30, + /* B&Q Consulting Station G2: https://wiki.uniteng.com/en/meshtastic/station-g2 */ + meshtastic_HardwareModel_STATION_G2 = 31, + /* --------------------------------------------------------------------------- + Less common/prototype boards listed here (needs one more byte over the air) + --------------------------------------------------------------------------- */ + meshtastic_HardwareModel_LORA_RELAY_V1 = 32, + /* TODO: REPLACE */ + meshtastic_HardwareModel_NRF52840DK = 33, + /* TODO: REPLACE */ + meshtastic_HardwareModel_PPR = 34, + /* TODO: REPLACE */ + meshtastic_HardwareModel_GENIEBLOCKS = 35, + /* TODO: REPLACE */ + meshtastic_HardwareModel_NRF52_UNKNOWN = 36, + /* TODO: REPLACE */ + meshtastic_HardwareModel_PORTDUINO = 37, + /* The simulator built into the android app */ + meshtastic_HardwareModel_ANDROID_SIM = 38, + /* Custom DIY device based on @NanoVHF schematics: https://github.com/NanoVHF/Meshtastic-DIY/tree/main/Schematics */ + meshtastic_HardwareModel_DIY_V1 = 39, + /* nRF52840 Dongle : https://www.nordicsemi.com/Products/Development-hardware/nrf52840-dongle/ */ + meshtastic_HardwareModel_NRF52840_PCA10059 = 40, + /* Custom Disaster Radio esp32 v3 device https://github.com/sudomesh/disaster-radio/tree/master/hardware/board_esp32_v3 */ + meshtastic_HardwareModel_DR_DEV = 41, + /* M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, CoreS3, Paper) https://m5stack.com/ */ + meshtastic_HardwareModel_M5STACK = 42, + /* New Heltec LoRA32 with ESP32-S3 CPU */ + meshtastic_HardwareModel_HELTEC_V3 = 43, + /* New Heltec Wireless Stick Lite with ESP32-S3 CPU */ + meshtastic_HardwareModel_HELTEC_WSL_V3 = 44, + /* New BETAFPV ELRS Micro TX Module 2.4G with ESP32 CPU */ + meshtastic_HardwareModel_BETAFPV_2400_TX = 45, + /* BetaFPV ExpressLRS "Nano" TX Module 900MHz with ESP32 CPU */ + meshtastic_HardwareModel_BETAFPV_900_NANO_TX = 46, + /* Raspberry Pi Pico (W) with Waveshare SX1262 LoRa Node Module */ + meshtastic_HardwareModel_RPI_PICO = 47, + /* Heltec Wireless Tracker with ESP32-S3 CPU, built-in GPS, and TFT + Newer V1.1, version is written on the PCB near the display. */ + meshtastic_HardwareModel_HELTEC_WIRELESS_TRACKER = 48, + /* Heltec Wireless Paper with ESP32-S3 CPU and E-Ink display */ + meshtastic_HardwareModel_HELTEC_WIRELESS_PAPER = 49, + /* LilyGo T-Deck with ESP32-S3 CPU, Keyboard and IPS display */ + meshtastic_HardwareModel_T_DECK = 50, + /* LilyGo T-Watch S3 with ESP32-S3 CPU and IPS display */ + meshtastic_HardwareModel_T_WATCH_S3 = 51, + /* Bobricius Picomputer with ESP32-S3 CPU, Keyboard and IPS display */ + meshtastic_HardwareModel_PICOMPUTER_S3 = 52, + /* Heltec HT-CT62 with ESP32-C3 CPU and SX1262 LoRa */ + meshtastic_HardwareModel_HELTEC_HT62 = 53, + /* EBYTE SPI LoRa module and ESP32-S3 */ + meshtastic_HardwareModel_EBYTE_ESP32_S3 = 54, + /* Waveshare ESP32-S3-PICO with PICO LoRa HAT and 2.9inch e-Ink */ + meshtastic_HardwareModel_ESP32_S3_PICO = 55, + /* CircuitMess Chatter 2 LLCC68 Lora Module and ESP32 Wroom + Lora module can be swapped out for a Heltec RA-62 which is "almost" pin compatible + with one cut and one jumper Meshtastic works */ + meshtastic_HardwareModel_CHATTER_2 = 56, + /* Heltec Wireless Paper, With ESP32-S3 CPU and E-Ink display + Older "V1.0" Variant, has no "version sticker" + E-Ink model is DEPG0213BNS800 + Tab on the screen protector is RED + Flex connector marking is FPC-7528B */ + meshtastic_HardwareModel_HELTEC_WIRELESS_PAPER_V1_0 = 57, + /* Heltec Wireless Tracker with ESP32-S3 CPU, built-in GPS, and TFT + Older "V1.0" Variant */ + meshtastic_HardwareModel_HELTEC_WIRELESS_TRACKER_V1_0 = 58, + /* unPhone with ESP32-S3, TFT touchscreen, LSM6DS3TR-C accelerometer and gyroscope */ + meshtastic_HardwareModel_UNPHONE = 59, + /* Teledatics TD-LORAC NRF52840 based M.2 LoRA module + Compatible with the TD-WRLS development board */ + meshtastic_HardwareModel_TD_LORAC = 60, + /* CDEBYTE EoRa-S3 board using their own MM modules, clone of LILYGO T3S3 */ + meshtastic_HardwareModel_CDEBYTE_EORA_S3 = 61, + /* TWC_MESH_V4 + Adafruit NRF52840 feather express with SX1262, SSD1306 OLED and NEO6M GPS */ + meshtastic_HardwareModel_TWC_MESH_V4 = 62, + /* NRF52_PROMICRO_DIY + Promicro NRF52840 with SX1262/LLCC68, SSD1306 OLED and NEO6M GPS */ + meshtastic_HardwareModel_NRF52_PROMICRO_DIY = 63, + /* RadioMaster 900 Bandit Nano, https://www.radiomasterrc.com/products/bandit-nano-expresslrs-rf-module + ESP32-D0WDQ6 With SX1276/SKY66122, SSD1306 OLED and No GPS */ + meshtastic_HardwareModel_RADIOMASTER_900_BANDIT_NANO = 64, + /* Heltec Capsule Sensor V3 with ESP32-S3 CPU, Portable LoRa device that can replace GNSS modules or sensors */ + meshtastic_HardwareModel_HELTEC_CAPSULE_SENSOR_V3 = 65, + /* Heltec Vision Master T190 with ESP32-S3 CPU, and a 1.90 inch TFT display */ + meshtastic_HardwareModel_HELTEC_VISION_MASTER_T190 = 66, + /* Heltec Vision Master E213 with ESP32-S3 CPU, and a 2.13 inch E-Ink display */ + meshtastic_HardwareModel_HELTEC_VISION_MASTER_E213 = 67, + /* Heltec Vision Master E290 with ESP32-S3 CPU, and a 2.9 inch E-Ink display */ + meshtastic_HardwareModel_HELTEC_VISION_MASTER_E290 = 68, + /* Heltec Mesh Node T114 board with nRF52840 CPU, and a 1.14 inch TFT display, Ultimate low-power design, + specifically adapted for the Meshtatic project */ + meshtastic_HardwareModel_HELTEC_MESH_NODE_T114 = 69, + /* Sensecap Indicator from Seeed Studio. ESP32-S3 device with TFT and RP2040 coprocessor */ + meshtastic_HardwareModel_SENSECAP_INDICATOR = 70, + /* Seeed studio T1000-E tracker card. NRF52840 w/ LR1110 radio, GPS, button, buzzer, and sensors. */ + meshtastic_HardwareModel_TRACKER_T1000_E = 71, + /* RAK3172 STM32WLE5 Module (https://store.rakwireless.com/products/wisduo-lpwan-module-rak3172) */ + meshtastic_HardwareModel_RAK3172 = 72, + /* Seeed Studio Wio-E5 (either mini or Dev kit) using STM32WL chip. */ + meshtastic_HardwareModel_WIO_E5 = 73, + /* RadioMaster 900 Bandit, https://www.radiomasterrc.com/products/bandit-expresslrs-rf-module + SSD1306 OLED and No GPS */ + meshtastic_HardwareModel_RADIOMASTER_900_BANDIT = 74, + /* Minewsemi ME25LS01 (ME25LE01_V1.0). NRF52840 w/ LR1110 radio, buttons and leds and pins. */ + meshtastic_HardwareModel_ME25LS01_4Y10TD = 75, + /* RP2040_FEATHER_RFM95 + Adafruit Feather RP2040 with RFM95 LoRa Radio RFM95 with SX1272, SSD1306 OLED + https://www.adafruit.com/product/5714 + https://www.adafruit.com/product/326 + https://www.adafruit.com/product/938 + ^^^ short A0 to switch to I2C address 0x3C */ + meshtastic_HardwareModel_RP2040_FEATHER_RFM95 = 76, + /* M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, CoreS3, Paper) https://m5stack.com/ */ + meshtastic_HardwareModel_M5STACK_COREBASIC = 77, + meshtastic_HardwareModel_M5STACK_CORE2 = 78, + /* Pico2 with Waveshare Hat, same as Pico */ + meshtastic_HardwareModel_RPI_PICO2 = 79, + /* M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, CoreS3, Paper) https://m5stack.com/ */ + meshtastic_HardwareModel_M5STACK_CORES3 = 80, + /* Seeed XIAO S3 DK */ + meshtastic_HardwareModel_SEEED_XIAO_S3 = 81, + /* Nordic nRF52840+Semtech SX1262 LoRa BLE Combo Module. nRF52840+SX1262 MS24SF1 */ + meshtastic_HardwareModel_MS24SF1 = 82, + /* Lilygo TLora-C6 with the new ESP32-C6 MCU */ + meshtastic_HardwareModel_TLORA_C6 = 83, + /* ------------------------------------------------------------------------------------------------------------------------------------------ + Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. + ------------------------------------------------------------------------------------------------------------------------------------------ */ + meshtastic_HardwareModel_PRIVATE_HW = 255 +} meshtastic_HardwareModel; + +/* Shared constants between device and phone */ +typedef enum _meshtastic_Constants { + /* First enum must be zero, and we are just using this enum to + pass int constants between two very different environments */ + meshtastic_Constants_ZERO = 0, + /* From mesh.options + note: this payload length is ONLY the bytes that are sent inside of the Data protobuf (excluding protobuf overhead). The 16 byte header is + outside of this envelope */ + meshtastic_Constants_DATA_PAYLOAD_LEN = 237 +} meshtastic_Constants; + +/* Error codes for critical errors + The device might report these fault codes on the screen. + If you encounter a fault code, please post on the meshtastic.discourse.group + and we'll try to help. */ +typedef enum _meshtastic_CriticalErrorCode { + /* TODO: REPLACE */ + meshtastic_CriticalErrorCode_NONE = 0, + /* A software bug was detected while trying to send lora */ + meshtastic_CriticalErrorCode_TX_WATCHDOG = 1, + /* A software bug was detected on entry to sleep */ + meshtastic_CriticalErrorCode_SLEEP_ENTER_WAIT = 2, + /* No Lora radio hardware could be found */ + meshtastic_CriticalErrorCode_NO_RADIO = 3, + /* Not normally used */ + meshtastic_CriticalErrorCode_UNSPECIFIED = 4, + /* We failed while configuring a UBlox GPS */ + meshtastic_CriticalErrorCode_UBLOX_UNIT_FAILED = 5, + /* This board was expected to have a power management chip and it is missing or broken */ + meshtastic_CriticalErrorCode_NO_AXP192 = 6, + /* The channel tried to set a radio setting which is not supported by this chipset, + radio comms settings are now undefined. */ + meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING = 7, + /* Radio transmit hardware failure. We sent data to the radio chip, but it didn't + reply with an interrupt. */ + meshtastic_CriticalErrorCode_TRANSMIT_FAILED = 8, + /* We detected that the main CPU voltage dropped below the minimum acceptable value */ + meshtastic_CriticalErrorCode_BROWNOUT = 9, + /* Selftest of SX1262 radio chip failed */ + meshtastic_CriticalErrorCode_SX1262_FAILURE = 10, + /* A (likely software but possibly hardware) failure was detected while trying to send packets. + If this occurs on your board, please post in the forum so that we can ask you to collect some information to allow fixing this bug */ + meshtastic_CriticalErrorCode_RADIO_SPI_BUG = 11, + /* Corruption was detected on the flash filesystem but we were able to repair things. + If you see this failure in the field please post in the forum because we are interested in seeing if this is occurring in the field. */ + meshtastic_CriticalErrorCode_FLASH_CORRUPTION_RECOVERABLE = 12, + /* Corruption was detected on the flash filesystem but we were unable to repair things. + NOTE: Your node will probably need to be reconfigured the next time it reboots (it will lose the region code etc...) + If you see this failure in the field please post in the forum because we are interested in seeing if this is occurring in the field. */ + meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE = 13 +} meshtastic_CriticalErrorCode; + +/* How the location was acquired: manual, onboard GPS, external (EUD) GPS */ +typedef enum _meshtastic_Position_LocSource { + /* TODO: REPLACE */ + meshtastic_Position_LocSource_LOC_UNSET = 0, + /* TODO: REPLACE */ + meshtastic_Position_LocSource_LOC_MANUAL = 1, + /* TODO: REPLACE */ + meshtastic_Position_LocSource_LOC_INTERNAL = 2, + /* TODO: REPLACE */ + meshtastic_Position_LocSource_LOC_EXTERNAL = 3 +} meshtastic_Position_LocSource; + +/* How the altitude was acquired: manual, GPS int/ext, etc + Default: same as location_source if present */ +typedef enum _meshtastic_Position_AltSource { + /* TODO: REPLACE */ + meshtastic_Position_AltSource_ALT_UNSET = 0, + /* TODO: REPLACE */ + meshtastic_Position_AltSource_ALT_MANUAL = 1, + /* TODO: REPLACE */ + meshtastic_Position_AltSource_ALT_INTERNAL = 2, + /* TODO: REPLACE */ + meshtastic_Position_AltSource_ALT_EXTERNAL = 3, + /* TODO: REPLACE */ + meshtastic_Position_AltSource_ALT_BAROMETRIC = 4 +} meshtastic_Position_AltSource; + +/* A failure in delivering a message (usually used for routing control messages, but might be provided in addition to ack.fail_id to provide + details on the type of failure). */ +typedef enum _meshtastic_Routing_Error { + /* This message is not a failure */ + meshtastic_Routing_Error_NONE = 0, + /* Our node doesn't have a route to the requested destination anymore. */ + meshtastic_Routing_Error_NO_ROUTE = 1, + /* We received a nak while trying to forward on your behalf */ + meshtastic_Routing_Error_GOT_NAK = 2, + /* TODO: REPLACE */ + meshtastic_Routing_Error_TIMEOUT = 3, + /* No suitable interface could be found for delivering this packet */ + meshtastic_Routing_Error_NO_INTERFACE = 4, + /* We reached the max retransmission count (typically for naive flood routing) */ + meshtastic_Routing_Error_MAX_RETRANSMIT = 5, + /* No suitable channel was found for sending this packet (i.e. was requested channel index disabled?) */ + meshtastic_Routing_Error_NO_CHANNEL = 6, + /* The packet was too big for sending (exceeds interface MTU after encoding) */ + meshtastic_Routing_Error_TOO_LARGE = 7, + /* The request had want_response set, the request reached the destination node, but no service on that node wants to send a response + (possibly due to bad channel permissions) */ + meshtastic_Routing_Error_NO_RESPONSE = 8, + /* Cannot send currently because duty cycle regulations will be violated. */ + meshtastic_Routing_Error_DUTY_CYCLE_LIMIT = 9, + /* The application layer service on the remote node received your request, but considered your request somehow invalid */ + meshtastic_Routing_Error_BAD_REQUEST = 32, + /* The application layer service on the remote node received your request, but considered your request not authorized + (i.e you did not send the request on the required bound channel) */ + meshtastic_Routing_Error_NOT_AUTHORIZED = 33, + /* The client specified a PKI transport, but the node was unable to send the packet using PKI (and did not send the message at all) */ + meshtastic_Routing_Error_PKI_FAILED = 34, + /* The receiving node does not have a Public Key to decode with */ + meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY = 35, + /* Admin packet otherwise checks out, but uses a bogus or expired session key */ + meshtastic_Routing_Error_ADMIN_BAD_SESSION_KEY = 36, + /* Admin packet sent using PKC, but not from a public key on the admin key list */ + meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED = 37 +} meshtastic_Routing_Error; + +/* The priority of this message for sending. + Higher priorities are sent first (when managing the transmit queue). + This field is never sent over the air, it is only used internally inside of a local device node. + API clients (either on the local node or connected directly to the node) + can set this parameter if necessary. + (values must be <= 127 to keep protobuf field to one byte in size. + Detailed background on this field: + I noticed a funny side effect of lora being so slow: Usually when making + a protocol there isn’t much need to use message priority to change the order + of transmission (because interfaces are fairly fast). + But for lora where packets can take a few seconds each, it is very important + to make sure that critical packets are sent ASAP. + In the case of meshtastic that means we want to send protocol acks as soon as possible + (to prevent unneeded retransmissions), we want routing messages to be sent next, + then messages marked as reliable and finally 'background' packets like periodic position updates. + So I bit the bullet and implemented a new (internal - not sent over the air) + field in MeshPacket called 'priority'. + And the transmission queue in the router object is now a priority queue. */ +typedef enum _meshtastic_MeshPacket_Priority { + /* Treated as Priority.DEFAULT */ + meshtastic_MeshPacket_Priority_UNSET = 0, + /* TODO: REPLACE */ + meshtastic_MeshPacket_Priority_MIN = 1, + /* Background position updates are sent with very low priority - + if the link is super congested they might not go out at all */ + meshtastic_MeshPacket_Priority_BACKGROUND = 10, + /* This priority is used for most messages that don't have a priority set */ + meshtastic_MeshPacket_Priority_DEFAULT = 64, + /* If priority is unset but the message is marked as want_ack, + assume it is important and use a slightly higher priority */ + meshtastic_MeshPacket_Priority_RELIABLE = 70, + /* If priority is unset but the packet is a response to a request, we want it to get there relatively quickly. + Furthermore, responses stop relaying packets directed to a node early. */ + meshtastic_MeshPacket_Priority_RESPONSE = 80, + /* Higher priority for specific message types (portnums) to distinguish between other reliable packets. */ + meshtastic_MeshPacket_Priority_HIGH = 100, + /* Ack/naks are sent with very high priority to ensure that retransmission + stops as soon as possible */ + meshtastic_MeshPacket_Priority_ACK = 120, + /* TODO: REPLACE */ + meshtastic_MeshPacket_Priority_MAX = 127 +} meshtastic_MeshPacket_Priority; + +/* Identify if this is a delayed packet */ +typedef enum _meshtastic_MeshPacket_Delayed { + /* If unset, the message is being sent in real time. */ + meshtastic_MeshPacket_Delayed_NO_DELAY = 0, + /* The message is delayed and was originally a broadcast */ + meshtastic_MeshPacket_Delayed_DELAYED_BROADCAST = 1, + /* The message is delayed and was originally a direct message */ + meshtastic_MeshPacket_Delayed_DELAYED_DIRECT = 2 +} meshtastic_MeshPacket_Delayed; + +/* Log levels, chosen to match python logging conventions. */ +typedef enum _meshtastic_LogRecord_Level { + /* Log levels, chosen to match python logging conventions. */ + meshtastic_LogRecord_Level_UNSET = 0, + /* Log levels, chosen to match python logging conventions. */ + meshtastic_LogRecord_Level_CRITICAL = 50, + /* Log levels, chosen to match python logging conventions. */ + meshtastic_LogRecord_Level_ERROR = 40, + /* Log levels, chosen to match python logging conventions. */ + meshtastic_LogRecord_Level_WARNING = 30, + /* Log levels, chosen to match python logging conventions. */ + meshtastic_LogRecord_Level_INFO = 20, + /* Log levels, chosen to match python logging conventions. */ + meshtastic_LogRecord_Level_DEBUG = 10, + /* Log levels, chosen to match python logging conventions. */ + meshtastic_LogRecord_Level_TRACE = 5 +} meshtastic_LogRecord_Level; + +/* Struct definitions */ +/* a gps position */ +typedef struct _meshtastic_Position { + /* The new preferred location encoding, multiply by 1e-7 to get degrees + in floating point */ + bool has_latitude_i; + int32_t latitude_i; + /* TODO: REPLACE */ + bool has_longitude_i; + int32_t longitude_i; + /* In meters above MSL (but see issue #359) */ + bool has_altitude; + int32_t altitude; + /* This is usually not sent over the mesh (to save space), but it is sent + from the phone so that the local device can set its time if it is sent over + the mesh (because there are devices on the mesh without GPS or RTC). + seconds since 1970 */ + uint32_t time; + /* TODO: REPLACE */ + meshtastic_Position_LocSource location_source; + /* TODO: REPLACE */ + meshtastic_Position_AltSource altitude_source; + /* Positional timestamp (actual timestamp of GPS solution) in integer epoch seconds */ + uint32_t timestamp; + /* Pos. timestamp milliseconds adjustment (rarely available or required) */ + int32_t timestamp_millis_adjust; + /* HAE altitude in meters - can be used instead of MSL altitude */ + bool has_altitude_hae; + int32_t altitude_hae; + /* Geoidal separation in meters */ + bool has_altitude_geoidal_separation; + int32_t altitude_geoidal_separation; + /* Horizontal, Vertical and Position Dilution of Precision, in 1/100 units + - PDOP is sufficient for most cases + - for higher precision scenarios, HDOP and VDOP can be used instead, + in which case PDOP becomes redundant (PDOP=sqrt(HDOP^2 + VDOP^2)) + TODO: REMOVE/INTEGRATE */ + uint32_t PDOP; + /* TODO: REPLACE */ + uint32_t HDOP; + /* TODO: REPLACE */ + uint32_t VDOP; + /* GPS accuracy (a hardware specific constant) in mm + multiplied with DOP to calculate positional accuracy + Default: "'bout three meters-ish" :) */ + uint32_t gps_accuracy; + /* Ground speed in m/s and True North TRACK in 1/100 degrees + Clarification of terms: + - "track" is the direction of motion (measured in horizontal plane) + - "heading" is where the fuselage points (measured in horizontal plane) + - "yaw" indicates a relative rotation about the vertical axis + TODO: REMOVE/INTEGRATE */ + bool has_ground_speed; + uint32_t ground_speed; + /* TODO: REPLACE */ + bool has_ground_track; + uint32_t ground_track; + /* GPS fix quality (from NMEA GxGGA statement or similar) */ + uint32_t fix_quality; + /* GPS fix type 2D/3D (from NMEA GxGSA statement) */ + uint32_t fix_type; + /* GPS "Satellites in View" number */ + uint32_t sats_in_view; + /* Sensor ID - in case multiple positioning sensors are being used */ + uint32_t sensor_id; + /* Estimated/expected time (in seconds) until next update: + - if we update at fixed intervals of X seconds, use X + - if we update at dynamic intervals (based on relative movement etc), + but "AT LEAST every Y seconds", use Y */ + uint32_t next_update; + /* A sequence number, incremented with each Position message to help + detect lost updates if needed */ + uint32_t seq_number; + /* Indicates the bits of precision set by the sending node */ + uint32_t precision_bits; +} meshtastic_Position; + +typedef PB_BYTES_ARRAY_T(32) meshtastic_User_public_key_t; +/* Broadcast when a newly powered mesh node wants to find a node num it can use + Sent from the phone over bluetooth to set the user id for the owner of this node. + Also sent from nodes to each other when a new node signs on (so all clients can have this info) + 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 nodedb) + If a node ever receives a User (not just the first broadcast) message where + the sender node number equals our node number, that indicates a collision has + occurred and the following steps should happen: + If the receiving node (that was already in the mesh)'s macaddr is LOWER than the + new User who just tried to sign in: it gets to keep its nodenum. + We send a broadcast message of OUR User (we use a broadcast so that the other node can + receive our message, considering we have the same id - it also serves to let + observers correct their nodedb) - this case is rare so it should be okay. + If any node receives a User where the macaddr is GTE than their local macaddr, + they have been vetoed and should pick a new random nodenum (filtering against + whatever it knows about the nodedb) and rebroadcast their User. + A few nodenums are reserved and will never be requested: + 0xff - broadcast + 0 through 3 - for future use */ +typedef struct _meshtastic_User { + /* A globally unique ID string for this user. + In the case of Signal that would mean +16504442323, for the default macaddr derived id it would be !<8 hexidecimal bytes>. + Note: app developers are encouraged to also use the following standard + node IDs "^all" (for broadcast), "^local" (for the locally connected node) */ + char id[16]; + /* A full name for this user, i.e. "Kevin Hester" */ + char long_name[40]; + /* A VERY short name, ideally two characters. + Suitable for a tiny OLED screen */ + char short_name[5]; + /* Deprecated in Meshtastic 2.1.x + This is the addr of the radio. + Not populated by the phone, but added by the esp32 when broadcasting */ + pb_byte_t macaddr[6]; + /* TBEAM, HELTEC, etc... + Starting in 1.2.11 moved to hw_model enum in the NodeInfo object. + Apps will still need the string here for older builds + (so OTA update can find the right image), but if the enum is available it will be used instead. */ + meshtastic_HardwareModel hw_model; + /* In some regions Ham radio operators have different bandwidth limitations than others. + If this user is a licensed operator, set this flag. + Also, "long_name" should be their licence number. */ + bool is_licensed; + /* Indicates that the user's role in the mesh */ + meshtastic_Config_DeviceConfig_Role role; + /* The public key of the user's device. + This is sent out to other nodes on the mesh to allow them to compute a shared secret key. */ + meshtastic_User_public_key_t public_key; +} meshtastic_User; + +/* A message used in a traceroute */ +typedef struct _meshtastic_RouteDiscovery { + /* The list of nodenums this packet has visited so far to the destination. */ + pb_size_t route_count; + uint32_t route[8]; + /* The list of SNRs (in dB, scaled by 4) in the route towards the destination. */ + pb_size_t snr_towards_count; + int8_t snr_towards[8]; + /* The list of nodenums the packet has visited on the way back from the destination. */ + pb_size_t route_back_count; + uint32_t route_back[8]; + /* The list of SNRs (in dB, scaled by 4) in the route back from the destination. */ + pb_size_t snr_back_count; + int8_t snr_back[8]; +} meshtastic_RouteDiscovery; + +/* A Routing control Data packet handled by the routing module */ +typedef struct _meshtastic_Routing { + pb_size_t which_variant; + union { + /* A route request going from the requester */ + meshtastic_RouteDiscovery route_request; + /* A route reply */ + meshtastic_RouteDiscovery route_reply; + /* A failure in delivering a message (usually used for routing control messages, but might be provided + in addition to ack.fail_id to provide details on the type of failure). */ + meshtastic_Routing_Error error_reason; + }; +} meshtastic_Routing; + +typedef PB_BYTES_ARRAY_T(237) meshtastic_Data_payload_t; +/* (Formerly called SubPacket) + The payload portion fo a packet, this is the actual bytes that are sent + inside a radio packet (because from/to are broken out by the comms library) */ +typedef struct _meshtastic_Data { + /* Formerly named typ and of type Type */ + meshtastic_PortNum portnum; + /* TODO: REPLACE */ + meshtastic_Data_payload_t payload; + /* Not normally used, but for testing a sender can request that recipient + responds in kind (i.e. if it received a position, it should unicast back it's position). + Note: that if you set this on a broadcast you will receive many replies. */ + bool want_response; + /* The address of the destination node. + This field is is filled in by the mesh radio device software, application + layer software should never need it. + RouteDiscovery messages _must_ populate this. + Other message types might need to if they are doing multihop routing. */ + uint32_t dest; + /* The address of the original sender for this message. + This field should _only_ be populated for reliable multihop packets (to keep + packets small). */ + uint32_t source; + /* Only used in routing or response messages. + Indicates the original message ID that this message is reporting failure on. (formerly called original_id) */ + uint32_t request_id; + /* If set, this message is intened to be a reply to a previously sent message with the defined id. */ + uint32_t reply_id; + /* Defaults to false. If true, then what is in the payload should be treated as an emoji like giving + a message a heart or poop emoji. */ + uint32_t emoji; + /* Bitfield for extra flags. First use is to indicate that user approves the packet being uploaded to MQTT. */ + bool has_bitfield; + uint8_t bitfield; +} meshtastic_Data; + +/* Waypoint message, used to share arbitrary locations across the mesh */ +typedef struct _meshtastic_Waypoint { + /* Id of the waypoint */ + uint32_t id; + /* latitude_i */ + bool has_latitude_i; + int32_t latitude_i; + /* longitude_i */ + bool has_longitude_i; + int32_t longitude_i; + /* Time the waypoint is to expire (epoch) */ + uint32_t expire; + /* If greater than zero, treat the value as a nodenum only allowing them to update the waypoint. + If zero, the waypoint is open to be edited by any member of the mesh. */ + uint32_t locked_to; + /* Name of the waypoint - max 30 chars */ + char name[30]; + /* Description of the waypoint - max 100 chars */ + char description[100]; + /* Designator icon for the waypoint in the form of a unicode emoji */ + uint32_t icon; +} meshtastic_Waypoint; + +typedef PB_BYTES_ARRAY_T(435) meshtastic_MqttClientProxyMessage_data_t; +/* This message will be proxied over the PhoneAPI for the client to deliver to the MQTT server */ +typedef struct _meshtastic_MqttClientProxyMessage { + /* The MQTT topic this message will be sent /received on */ + char topic[60]; + pb_size_t which_payload_variant; + union { + /* Bytes */ + meshtastic_MqttClientProxyMessage_data_t data; + /* Text */ + char text[435]; + } payload_variant; + /* Whether the message should be retained (or not) */ + bool retained; +} meshtastic_MqttClientProxyMessage; + +typedef PB_BYTES_ARRAY_T(256) meshtastic_MeshPacket_encrypted_t; +typedef PB_BYTES_ARRAY_T(32) meshtastic_MeshPacket_public_key_t; +/* A packet envelope sent/received over the mesh + only payload_variant is sent in the payload portion of the LORA packet. + The other fields are either not sent at all, or sent in the special 16 byte LORA header. */ +typedef struct _meshtastic_MeshPacket { + /* The sending node number. + Note: Our crypto implementation uses this field as well. + See [crypto](/docs/overview/encryption) for details. */ + uint32_t from; + /* The (immediate) destination for this packet */ + uint32_t to; + /* (Usually) If set, this indicates the index in the secondary_channels table that this packet was sent/received on. + If unset, packet was on the primary channel. + A particular node might know only a subset of channels in use on the mesh. + Therefore channel_index is inherently a local concept and meaningless to send between nodes. + Very briefly, while sending and receiving deep inside the device Router code, this field instead + contains the 'channel hash' instead of the index. + This 'trick' is only used while the payload_variant is an 'encrypted'. */ + uint8_t channel; + pb_size_t which_payload_variant; + union { + /* TODO: REPLACE */ + meshtastic_Data decoded; + /* TODO: REPLACE */ + meshtastic_MeshPacket_encrypted_t encrypted; + }; + /* A unique ID for this packet. + Always 0 for no-ack packets or non broadcast packets (and therefore take zero bytes of space). + Otherwise a unique ID for this packet, useful for flooding algorithms. + ID only needs to be unique on a _per sender_ basis, and it only + needs to be unique for a few minutes (long enough to last for the length of + any ACK or the completion of a mesh broadcast flood). + Note: Our crypto implementation uses this id as well. + See [crypto](/docs/overview/encryption) for details. */ + uint32_t id; + /* The time this message was received by the esp32 (secs since 1970). + Note: this field is _never_ sent on the radio link itself (to save space) Times + are typically not sent over the mesh, but they will be added to any Packet + (chain of SubPacket) sent to the phone (so the phone can know exact time of reception) */ + uint32_t rx_time; + /* *Never* sent over the radio links. + Set during reception to indicate the SNR of this packet. + Used to collect statistics on current link quality. */ + float rx_snr; + /* If unset treated as zero (no forwarding, send to adjacent nodes only) + if 1, allow hopping through one node, etc... + For our usecase real world topologies probably have a max of about 3. + This field is normally placed into a few of bits in the header. */ + uint8_t hop_limit; + /* This packet is being sent as a reliable message, we would prefer it to arrive at the destination. + We would like to receive a ack packet in response. + Broadcasts messages treat this flag specially: Since acks for broadcasts would + rapidly flood the channel, the normal ack behavior is suppressed. + Instead, the original sender listens to see if at least one node is rebroadcasting this packet (because naive flooding algorithm). + If it hears that the odds (given typical LoRa topologies) the odds are very high that every node should eventually receive the message. + So FloodingRouter.cpp generates an implicit ack which is delivered to the original sender. + If after some time we don't hear anyone rebroadcast our packet, we will timeout and retransmit, using the regular resend logic. + Note: This flag is normally sent in a flag bit in the header when sent over the wire */ + bool want_ack; + /* The priority of this message for sending. + See MeshPacket.Priority description for more details. */ + meshtastic_MeshPacket_Priority priority; + /* rssi of received packet. Only sent to phone for dispay purposes. */ + int32_t rx_rssi; + /* Describe if this message is delayed */ + meshtastic_MeshPacket_Delayed delayed; + /* Describes whether this packet passed via MQTT somewhere along the path it currently took. */ + bool via_mqtt; + /* Hop limit with which the original packet started. Sent via LoRa using three bits in the unencrypted header. + When receiving a packet, the difference between hop_start and hop_limit gives how many hops it traveled. */ + uint8_t hop_start; + /* Records the public key the packet was encrypted with, if applicable. */ + meshtastic_MeshPacket_public_key_t public_key; + /* Indicates whether the packet was en/decrypted using PKI */ + bool pki_encrypted; +} meshtastic_MeshPacket; + +/* The bluetooth to device link: + Old BTLE protocol docs from TODO, merge in above and make real docs... + use protocol buffers, and NanoPB + messages from device to phone: + POSITION_UPDATE (..., time) + TEXT_RECEIVED(from, text, time) + OPAQUE_RECEIVED(from, payload, time) (for signal messages or other applications) + messages from phone to device: + SET_MYID(id, human readable long, human readable short) (send down the unique ID + string used for this node, a human readable string shown for that id, and a very + short human readable string suitable for oled screen) SEND_OPAQUE(dest, payload) + (for signal messages or other applications) SEND_TEXT(dest, text) Get all + nodes() (returns list of nodes, with full info, last time seen, loc, battery + level etc) SET_CONFIG (switches device to a new set of radio params and + preshared key, drops all existing nodes, force our node to rejoin this new group) + Full information about a node on the mesh */ +typedef struct _meshtastic_NodeInfo { + /* The node number */ + uint32_t num; + /* The user info for this node */ + bool has_user; + meshtastic_User user; + /* This position data. Note: before 1.2.14 we would also store the last time we've heard from this node in position.time, that is no longer true. + Position.time now indicates the last time we received a POSITION from that node. */ + bool has_position; + meshtastic_Position position; + /* Returns the Signal-to-noise ratio (SNR) of the last received message, + as measured by the receiver. Return SNR of the last received message in dB */ + float snr; + /* Set to indicate the last time we received a packet from this node */ + uint32_t last_heard; + /* The latest device metrics for the node. */ + bool has_device_metrics; + meshtastic_DeviceMetrics device_metrics; + /* local channel index we heard that node on. Only populated if its not the default channel. */ + uint8_t channel; + /* True if we witnessed the node over MQTT instead of LoRA transport */ + bool via_mqtt; + /* Number of hops away from us this node is (0 if adjacent) */ + bool has_hops_away; + uint8_t hops_away; + /* True if node is in our favorites list + Persists between NodeDB internal clean ups */ + bool is_favorite; +} meshtastic_NodeInfo; + +typedef PB_BYTES_ARRAY_T(16) meshtastic_MyNodeInfo_device_id_t; +/* Unique local debugging info for this node + Note: we don't include position or the user info, because that will come in the + Sent to the phone in response to WantNodes. */ +typedef struct _meshtastic_MyNodeInfo { + /* Tells the phone what our node number is, default starting value is + lowbyte of macaddr, but it will be fixed if that is already in use */ + uint32_t my_node_num; + /* The total number of reboots this node has ever encountered + (well - since the last time we discarded preferences) */ + uint32_t reboot_count; + /* The minimum app version that can talk to this device. + Phone/PC apps should compare this to their build number and if too low tell the user they must update their app */ + uint32_t min_app_version; + /* Unique hardware identifier for this device */ + meshtastic_MyNodeInfo_device_id_t device_id; +} meshtastic_MyNodeInfo; + +/* Debug output from the device. + To minimize the size of records inside the device code, if a time/source/level is not set + on the message it is assumed to be a continuation of the previously sent message. + This allows the device code to use fixed maxlen 64 byte strings for messages, + and then extend as needed by emitting multiple records. */ +typedef struct _meshtastic_LogRecord { + /* Log levels, chosen to match python logging conventions. */ + char message[384]; + /* Seconds since 1970 - or 0 for unknown/unset */ + uint32_t time; + /* Usually based on thread name - if known */ + char source[32]; + /* Not yet set */ + meshtastic_LogRecord_Level level; +} meshtastic_LogRecord; + +typedef struct _meshtastic_QueueStatus { + /* Last attempt to queue status, ErrorCode */ + int8_t res; + /* Free entries in the outgoing queue */ + uint8_t free; + /* Maximum entries in the outgoing queue */ + uint8_t maxlen; + /* What was mesh packet id that generated this response? */ + uint32_t mesh_packet_id; +} meshtastic_QueueStatus; + +/* A notification message from the device to the client + To be used for important messages that should to be displayed to the user + in the form of push notifications or validation messages when saving + invalid configuration. */ +typedef struct _meshtastic_ClientNotification { + /* The id of the packet we're notifying in response to */ + bool has_reply_id; + uint32_t reply_id; + /* Seconds since 1970 - or 0 for unknown/unset */ + uint32_t time; + /* The level type of notification */ + meshtastic_LogRecord_Level level; + /* The message body of the notification */ + char message[400]; +} meshtastic_ClientNotification; + +/* Individual File info for the device */ +typedef struct _meshtastic_FileInfo { + /* The fully qualified path of the file */ + char file_name[228]; + /* The size of the file in bytes */ + uint32_t size_bytes; +} meshtastic_FileInfo; + +typedef PB_BYTES_ARRAY_T(237) meshtastic_Compressed_data_t; +/* Compressed message payload */ +typedef struct _meshtastic_Compressed { + /* PortNum to determine the how to handle the compressed payload. */ + meshtastic_PortNum portnum; + /* Compressed data. */ + meshtastic_Compressed_data_t data; +} meshtastic_Compressed; + +/* A single edge in the mesh */ +typedef struct _meshtastic_Neighbor { + /* Node ID of neighbor */ + uint32_t node_id; + /* SNR of last heard message */ + float snr; + /* Reception time (in secs since 1970) of last message that was last sent by this ID. + Note: this is for local storage only and will not be sent out over the mesh. */ + uint32_t last_rx_time; + /* Broadcast interval of this neighbor (in seconds). + Note: this is for local storage only and will not be sent out over the mesh. */ + uint32_t node_broadcast_interval_secs; +} meshtastic_Neighbor; + +/* Full info on edges for a single node */ +typedef struct _meshtastic_NeighborInfo { + /* The node ID of the node sending info on its neighbors */ + uint32_t node_id; + /* Field to pass neighbor info for the next sending cycle */ + uint32_t last_sent_by_id; + /* Broadcast interval of the represented node (in seconds) */ + uint32_t node_broadcast_interval_secs; + /* The list of out edges from this node */ + pb_size_t neighbors_count; + meshtastic_Neighbor neighbors[10]; +} meshtastic_NeighborInfo; + +/* Device metadata response */ +typedef struct _meshtastic_DeviceMetadata { + /* Device firmware version string */ + char firmware_version[18]; + /* Device state version */ + uint32_t device_state_version; + /* Indicates whether the device can shutdown CPU natively or via power management chip */ + bool canShutdown; + /* Indicates that the device has native wifi capability */ + bool hasWifi; + /* Indicates that the device has native bluetooth capability */ + bool hasBluetooth; + /* Indicates that the device has an ethernet peripheral */ + bool hasEthernet; + /* Indicates that the device's role in the mesh */ + meshtastic_Config_DeviceConfig_Role role; + /* Indicates the device's current enabled position flags */ + uint32_t position_flags; + /* Device hardware model */ + meshtastic_HardwareModel hw_model; + /* Has Remote Hardware enabled */ + bool hasRemoteHardware; + /* Has PKC capabilities */ + bool hasPKC; +} meshtastic_DeviceMetadata; + +/* Packets from the radio to the phone will appear on the fromRadio characteristic. + It will support READ and NOTIFY. When a new packet arrives the device will BLE notify? + It will sit in that descriptor until consumed by the phone, + at which point the next item in the FIFO will be populated. */ +typedef struct _meshtastic_FromRadio { + /* The packet id, used to allow the phone to request missing read packets from the FIFO, + see our bluetooth docs */ + uint32_t id; + pb_size_t which_payload_variant; + union { + /* Log levels, chosen to match python logging conventions. */ + meshtastic_MeshPacket packet; + /* Tells the phone what our node number is, can be -1 if we've not yet joined a mesh. + NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps. */ + meshtastic_MyNodeInfo my_info; + /* One packet is sent for each node in the on radio DB + starts over with the first node in our DB */ + meshtastic_NodeInfo node_info; + /* Include a part of the config (was: RadioConfig radio) */ + meshtastic_Config config; + /* Set to send debug console output over our protobuf stream */ + meshtastic_LogRecord log_record; + /* Sent as true once the device has finished sending all of the responses to want_config + recipient should check if this ID matches our original request nonce, if + not, it means your config responses haven't started yet. + NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps. */ + uint32_t config_complete_id; + /* Sent to tell clients the radio has just rebooted. + Set to true if present. + Not used on all transports, currently just used for the serial console. + NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps. */ + bool rebooted; + /* Include module config */ + meshtastic_ModuleConfig moduleConfig; + /* One packet is sent for each channel */ + meshtastic_Channel channel; + /* Queue status info */ + meshtastic_QueueStatus queueStatus; + /* File Transfer Chunk */ + meshtastic_XModem xmodemPacket; + /* Device metadata message */ + meshtastic_DeviceMetadata metadata; + /* MQTT Client Proxy Message (device sending to client / phone for publishing to MQTT) */ + meshtastic_MqttClientProxyMessage mqttClientProxyMessage; + /* File system manifest messages */ + meshtastic_FileInfo fileInfo; + /* Notification message to the client */ + meshtastic_ClientNotification clientNotification; + /* Persistent data for device-ui */ + meshtastic_DeviceUIConfig deviceuiConfig; + }; +} meshtastic_FromRadio; + +/* A heartbeat message is sent to the node from the client to keep the connection alive. + This is currently only needed to keep serial connections alive, but can be used by any PhoneAPI. */ +typedef struct _meshtastic_Heartbeat { + char dummy_field; +} meshtastic_Heartbeat; + +/* Packets/commands to the radio will be written (reliably) to the toRadio characteristic. + Once the write completes the phone can assume it is handled. */ +typedef struct _meshtastic_ToRadio { + pb_size_t which_payload_variant; + union { + /* Send this packet on the mesh */ + meshtastic_MeshPacket packet; + /* Phone wants radio to send full node db to the phone, This is + typically the first packet sent to the radio when the phone gets a + bluetooth connection. The radio will respond by sending back a + MyNodeInfo, a owner, a radio config and a series of + FromRadio.node_infos, and config_complete + the integer you write into this field will be reported back in the + config_complete_id response this allows clients to never be confused by + a stale old partially sent config. */ + uint32_t want_config_id; + /* Tell API server we are disconnecting now. + This is useful for serial links where there is no hardware/protocol based notification that the client has dropped the link. + (Sending this message is optional for clients) */ + bool disconnect; + meshtastic_XModem xmodemPacket; + /* MQTT Client Proxy Message (for client / phone subscribed to MQTT sending to device) */ + meshtastic_MqttClientProxyMessage mqttClientProxyMessage; + /* Heartbeat message (used to keep the device connection awake on serial) */ + meshtastic_Heartbeat heartbeat; + }; +} meshtastic_ToRadio; + +/* RemoteHardwarePins associated with a node */ +typedef struct _meshtastic_NodeRemoteHardwarePin { + /* The node_num exposing the available gpio pin */ + uint32_t node_num; + /* The the available gpio pin for usage with RemoteHardware module */ + bool has_pin; + meshtastic_RemoteHardwarePin pin; +} meshtastic_NodeRemoteHardwarePin; + +typedef PB_BYTES_ARRAY_T(228) meshtastic_ChunkedPayload_payload_chunk_t; +typedef struct _meshtastic_ChunkedPayload { + /* The ID of the entire payload */ + uint32_t payload_id; + /* The total number of chunks in the payload */ + uint16_t chunk_count; + /* The current chunk index in the total */ + uint16_t chunk_index; + /* The binary data of the current chunk */ + meshtastic_ChunkedPayload_payload_chunk_t payload_chunk; +} meshtastic_ChunkedPayload; + +/* Wrapper message for broken repeated oneof support */ +typedef struct _meshtastic_resend_chunks { + pb_callback_t chunks; +} meshtastic_resend_chunks; + +/* Responses to a ChunkedPayload request */ +typedef struct _meshtastic_ChunkedPayloadResponse { + /* The ID of the entire payload */ + uint32_t payload_id; + pb_size_t which_payload_variant; + union { + /* Request to transfer chunked payload */ + bool request_transfer; + /* Accept the transfer chunked payload */ + bool accept_transfer; + /* Request missing indexes in the chunked payload */ + meshtastic_resend_chunks resend_chunks; + } payload_variant; +} meshtastic_ChunkedPayloadResponse; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Helper constants for enums */ +#define _meshtastic_HardwareModel_MIN meshtastic_HardwareModel_UNSET +#define _meshtastic_HardwareModel_MAX meshtastic_HardwareModel_PRIVATE_HW +#define _meshtastic_HardwareModel_ARRAYSIZE ((meshtastic_HardwareModel)(meshtastic_HardwareModel_PRIVATE_HW+1)) + +#define _meshtastic_Constants_MIN meshtastic_Constants_ZERO +#define _meshtastic_Constants_MAX meshtastic_Constants_DATA_PAYLOAD_LEN +#define _meshtastic_Constants_ARRAYSIZE ((meshtastic_Constants)(meshtastic_Constants_DATA_PAYLOAD_LEN+1)) + +#define _meshtastic_CriticalErrorCode_MIN meshtastic_CriticalErrorCode_NONE +#define _meshtastic_CriticalErrorCode_MAX meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE +#define _meshtastic_CriticalErrorCode_ARRAYSIZE ((meshtastic_CriticalErrorCode)(meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE+1)) + +#define _meshtastic_Position_LocSource_MIN meshtastic_Position_LocSource_LOC_UNSET +#define _meshtastic_Position_LocSource_MAX meshtastic_Position_LocSource_LOC_EXTERNAL +#define _meshtastic_Position_LocSource_ARRAYSIZE ((meshtastic_Position_LocSource)(meshtastic_Position_LocSource_LOC_EXTERNAL+1)) + +#define _meshtastic_Position_AltSource_MIN meshtastic_Position_AltSource_ALT_UNSET +#define _meshtastic_Position_AltSource_MAX meshtastic_Position_AltSource_ALT_BAROMETRIC +#define _meshtastic_Position_AltSource_ARRAYSIZE ((meshtastic_Position_AltSource)(meshtastic_Position_AltSource_ALT_BAROMETRIC+1)) + +#define _meshtastic_Routing_Error_MIN meshtastic_Routing_Error_NONE +#define _meshtastic_Routing_Error_MAX meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED +#define _meshtastic_Routing_Error_ARRAYSIZE ((meshtastic_Routing_Error)(meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED+1)) + +#define _meshtastic_MeshPacket_Priority_MIN meshtastic_MeshPacket_Priority_UNSET +#define _meshtastic_MeshPacket_Priority_MAX meshtastic_MeshPacket_Priority_MAX +#define _meshtastic_MeshPacket_Priority_ARRAYSIZE ((meshtastic_MeshPacket_Priority)(meshtastic_MeshPacket_Priority_MAX+1)) + +#define _meshtastic_MeshPacket_Delayed_MIN meshtastic_MeshPacket_Delayed_NO_DELAY +#define _meshtastic_MeshPacket_Delayed_MAX meshtastic_MeshPacket_Delayed_DELAYED_DIRECT +#define _meshtastic_MeshPacket_Delayed_ARRAYSIZE ((meshtastic_MeshPacket_Delayed)(meshtastic_MeshPacket_Delayed_DELAYED_DIRECT+1)) + +#define _meshtastic_LogRecord_Level_MIN meshtastic_LogRecord_Level_UNSET +#define _meshtastic_LogRecord_Level_MAX meshtastic_LogRecord_Level_CRITICAL +#define _meshtastic_LogRecord_Level_ARRAYSIZE ((meshtastic_LogRecord_Level)(meshtastic_LogRecord_Level_CRITICAL+1)) + +#define meshtastic_Position_location_source_ENUMTYPE meshtastic_Position_LocSource +#define meshtastic_Position_altitude_source_ENUMTYPE meshtastic_Position_AltSource + +#define meshtastic_User_hw_model_ENUMTYPE meshtastic_HardwareModel +#define meshtastic_User_role_ENUMTYPE meshtastic_Config_DeviceConfig_Role + + +#define meshtastic_Routing_variant_error_reason_ENUMTYPE meshtastic_Routing_Error + +#define meshtastic_Data_portnum_ENUMTYPE meshtastic_PortNum + + + +#define meshtastic_MeshPacket_priority_ENUMTYPE meshtastic_MeshPacket_Priority +#define meshtastic_MeshPacket_delayed_ENUMTYPE meshtastic_MeshPacket_Delayed + + + +#define meshtastic_LogRecord_level_ENUMTYPE meshtastic_LogRecord_Level + + + +#define meshtastic_ClientNotification_level_ENUMTYPE meshtastic_LogRecord_Level + + + +#define meshtastic_Compressed_portnum_ENUMTYPE meshtastic_PortNum + + + +#define meshtastic_DeviceMetadata_role_ENUMTYPE meshtastic_Config_DeviceConfig_Role +#define meshtastic_DeviceMetadata_hw_model_ENUMTYPE meshtastic_HardwareModel + + + + + + + +/* Initializer values for message structs */ +#define meshtastic_Position_init_default {false, 0, false, 0, false, 0, 0, _meshtastic_Position_LocSource_MIN, _meshtastic_Position_AltSource_MIN, 0, 0, false, 0, false, 0, 0, 0, 0, 0, false, 0, false, 0, 0, 0, 0, 0, 0, 0, 0} +#define meshtastic_User_init_default {"", "", "", {0}, _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}} +#define meshtastic_RouteDiscovery_init_default {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}} +#define meshtastic_Routing_init_default {0, {meshtastic_RouteDiscovery_init_default}} +#define meshtastic_Data_init_default {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0} +#define meshtastic_Waypoint_init_default {0, false, 0, false, 0, 0, 0, "", "", 0} +#define meshtastic_MqttClientProxyMessage_init_default {"", 0, {{0, {0}}}, 0} +#define meshtastic_MeshPacket_init_default {0, 0, 0, 0, {meshtastic_Data_init_default}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0} +#define meshtastic_NodeInfo_init_default {0, false, meshtastic_User_init_default, false, meshtastic_Position_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0} +#define meshtastic_MyNodeInfo_init_default {0, 0, 0, {0, {0}}} +#define meshtastic_LogRecord_init_default {"", 0, "", _meshtastic_LogRecord_Level_MIN} +#define meshtastic_QueueStatus_init_default {0, 0, 0, 0} +#define meshtastic_FromRadio_init_default {0, 0, {meshtastic_MeshPacket_init_default}} +#define meshtastic_ClientNotification_init_default {false, 0, 0, _meshtastic_LogRecord_Level_MIN, ""} +#define meshtastic_FileInfo_init_default {"", 0} +#define meshtastic_ToRadio_init_default {0, {meshtastic_MeshPacket_init_default}} +#define meshtastic_Compressed_init_default {_meshtastic_PortNum_MIN, {0, {0}}} +#define meshtastic_NeighborInfo_init_default {0, 0, 0, 0, {meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default}} +#define meshtastic_Neighbor_init_default {0, 0, 0, 0} +#define meshtastic_DeviceMetadata_init_default {"", 0, 0, 0, 0, 0, _meshtastic_Config_DeviceConfig_Role_MIN, 0, _meshtastic_HardwareModel_MIN, 0, 0} +#define meshtastic_Heartbeat_init_default {0} +#define meshtastic_NodeRemoteHardwarePin_init_default {0, false, meshtastic_RemoteHardwarePin_init_default} +#define meshtastic_ChunkedPayload_init_default {0, 0, 0, {0, {0}}} +#define meshtastic_resend_chunks_init_default {{{NULL}, NULL}} +#define meshtastic_ChunkedPayloadResponse_init_default {0, 0, {0}} +#define meshtastic_Position_init_zero {false, 0, false, 0, false, 0, 0, _meshtastic_Position_LocSource_MIN, _meshtastic_Position_AltSource_MIN, 0, 0, false, 0, false, 0, 0, 0, 0, 0, false, 0, false, 0, 0, 0, 0, 0, 0, 0, 0} +#define meshtastic_User_init_zero {"", "", "", {0}, _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}} +#define meshtastic_RouteDiscovery_init_zero {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}} +#define meshtastic_Routing_init_zero {0, {meshtastic_RouteDiscovery_init_zero}} +#define meshtastic_Data_init_zero {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0} +#define meshtastic_Waypoint_init_zero {0, false, 0, false, 0, 0, 0, "", "", 0} +#define meshtastic_MqttClientProxyMessage_init_zero {"", 0, {{0, {0}}}, 0} +#define meshtastic_MeshPacket_init_zero {0, 0, 0, 0, {meshtastic_Data_init_zero}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0} +#define meshtastic_NodeInfo_init_zero {0, false, meshtastic_User_init_zero, false, meshtastic_Position_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0} +#define meshtastic_MyNodeInfo_init_zero {0, 0, 0, {0, {0}}} +#define meshtastic_LogRecord_init_zero {"", 0, "", _meshtastic_LogRecord_Level_MIN} +#define meshtastic_QueueStatus_init_zero {0, 0, 0, 0} +#define meshtastic_FromRadio_init_zero {0, 0, {meshtastic_MeshPacket_init_zero}} +#define meshtastic_ClientNotification_init_zero {false, 0, 0, _meshtastic_LogRecord_Level_MIN, ""} +#define meshtastic_FileInfo_init_zero {"", 0} +#define meshtastic_ToRadio_init_zero {0, {meshtastic_MeshPacket_init_zero}} +#define meshtastic_Compressed_init_zero {_meshtastic_PortNum_MIN, {0, {0}}} +#define meshtastic_NeighborInfo_init_zero {0, 0, 0, 0, {meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero}} +#define meshtastic_Neighbor_init_zero {0, 0, 0, 0} +#define meshtastic_DeviceMetadata_init_zero {"", 0, 0, 0, 0, 0, _meshtastic_Config_DeviceConfig_Role_MIN, 0, _meshtastic_HardwareModel_MIN, 0, 0} +#define meshtastic_Heartbeat_init_zero {0} +#define meshtastic_NodeRemoteHardwarePin_init_zero {0, false, meshtastic_RemoteHardwarePin_init_zero} +#define meshtastic_ChunkedPayload_init_zero {0, 0, 0, {0, {0}}} +#define meshtastic_resend_chunks_init_zero {{{NULL}, NULL}} +#define meshtastic_ChunkedPayloadResponse_init_zero {0, 0, {0}} + +/* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_Position_latitude_i_tag 1 +#define meshtastic_Position_longitude_i_tag 2 +#define meshtastic_Position_altitude_tag 3 +#define meshtastic_Position_time_tag 4 +#define meshtastic_Position_location_source_tag 5 +#define meshtastic_Position_altitude_source_tag 6 +#define meshtastic_Position_timestamp_tag 7 +#define meshtastic_Position_timestamp_millis_adjust_tag 8 +#define meshtastic_Position_altitude_hae_tag 9 +#define meshtastic_Position_altitude_geoidal_separation_tag 10 +#define meshtastic_Position_PDOP_tag 11 +#define meshtastic_Position_HDOP_tag 12 +#define meshtastic_Position_VDOP_tag 13 +#define meshtastic_Position_gps_accuracy_tag 14 +#define meshtastic_Position_ground_speed_tag 15 +#define meshtastic_Position_ground_track_tag 16 +#define meshtastic_Position_fix_quality_tag 17 +#define meshtastic_Position_fix_type_tag 18 +#define meshtastic_Position_sats_in_view_tag 19 +#define meshtastic_Position_sensor_id_tag 20 +#define meshtastic_Position_next_update_tag 21 +#define meshtastic_Position_seq_number_tag 22 +#define meshtastic_Position_precision_bits_tag 23 +#define meshtastic_User_id_tag 1 +#define meshtastic_User_long_name_tag 2 +#define meshtastic_User_short_name_tag 3 +#define meshtastic_User_macaddr_tag 4 +#define meshtastic_User_hw_model_tag 5 +#define meshtastic_User_is_licensed_tag 6 +#define meshtastic_User_role_tag 7 +#define meshtastic_User_public_key_tag 8 +#define meshtastic_RouteDiscovery_route_tag 1 +#define meshtastic_RouteDiscovery_snr_towards_tag 2 +#define meshtastic_RouteDiscovery_route_back_tag 3 +#define meshtastic_RouteDiscovery_snr_back_tag 4 +#define meshtastic_Routing_route_request_tag 1 +#define meshtastic_Routing_route_reply_tag 2 +#define meshtastic_Routing_error_reason_tag 3 +#define meshtastic_Data_portnum_tag 1 +#define meshtastic_Data_payload_tag 2 +#define meshtastic_Data_want_response_tag 3 +#define meshtastic_Data_dest_tag 4 +#define meshtastic_Data_source_tag 5 +#define meshtastic_Data_request_id_tag 6 +#define meshtastic_Data_reply_id_tag 7 +#define meshtastic_Data_emoji_tag 8 +#define meshtastic_Data_bitfield_tag 9 +#define meshtastic_Waypoint_id_tag 1 +#define meshtastic_Waypoint_latitude_i_tag 2 +#define meshtastic_Waypoint_longitude_i_tag 3 +#define meshtastic_Waypoint_expire_tag 4 +#define meshtastic_Waypoint_locked_to_tag 5 +#define meshtastic_Waypoint_name_tag 6 +#define meshtastic_Waypoint_description_tag 7 +#define meshtastic_Waypoint_icon_tag 8 +#define meshtastic_MqttClientProxyMessage_topic_tag 1 +#define meshtastic_MqttClientProxyMessage_data_tag 2 +#define meshtastic_MqttClientProxyMessage_text_tag 3 +#define meshtastic_MqttClientProxyMessage_retained_tag 4 +#define meshtastic_MeshPacket_from_tag 1 +#define meshtastic_MeshPacket_to_tag 2 +#define meshtastic_MeshPacket_channel_tag 3 +#define meshtastic_MeshPacket_decoded_tag 4 +#define meshtastic_MeshPacket_encrypted_tag 5 +#define meshtastic_MeshPacket_id_tag 6 +#define meshtastic_MeshPacket_rx_time_tag 7 +#define meshtastic_MeshPacket_rx_snr_tag 8 +#define meshtastic_MeshPacket_hop_limit_tag 9 +#define meshtastic_MeshPacket_want_ack_tag 10 +#define meshtastic_MeshPacket_priority_tag 11 +#define meshtastic_MeshPacket_rx_rssi_tag 12 +#define meshtastic_MeshPacket_delayed_tag 13 +#define meshtastic_MeshPacket_via_mqtt_tag 14 +#define meshtastic_MeshPacket_hop_start_tag 15 +#define meshtastic_MeshPacket_public_key_tag 16 +#define meshtastic_MeshPacket_pki_encrypted_tag 17 +#define meshtastic_NodeInfo_num_tag 1 +#define meshtastic_NodeInfo_user_tag 2 +#define meshtastic_NodeInfo_position_tag 3 +#define meshtastic_NodeInfo_snr_tag 4 +#define meshtastic_NodeInfo_last_heard_tag 5 +#define meshtastic_NodeInfo_device_metrics_tag 6 +#define meshtastic_NodeInfo_channel_tag 7 +#define meshtastic_NodeInfo_via_mqtt_tag 8 +#define meshtastic_NodeInfo_hops_away_tag 9 +#define meshtastic_NodeInfo_is_favorite_tag 10 +#define meshtastic_MyNodeInfo_my_node_num_tag 1 +#define meshtastic_MyNodeInfo_reboot_count_tag 8 +#define meshtastic_MyNodeInfo_min_app_version_tag 11 +#define meshtastic_MyNodeInfo_device_id_tag 12 +#define meshtastic_LogRecord_message_tag 1 +#define meshtastic_LogRecord_time_tag 2 +#define meshtastic_LogRecord_source_tag 3 +#define meshtastic_LogRecord_level_tag 4 +#define meshtastic_QueueStatus_res_tag 1 +#define meshtastic_QueueStatus_free_tag 2 +#define meshtastic_QueueStatus_maxlen_tag 3 +#define meshtastic_QueueStatus_mesh_packet_id_tag 4 +#define meshtastic_ClientNotification_reply_id_tag 1 +#define meshtastic_ClientNotification_time_tag 2 +#define meshtastic_ClientNotification_level_tag 3 +#define meshtastic_ClientNotification_message_tag 4 +#define meshtastic_FileInfo_file_name_tag 1 +#define meshtastic_FileInfo_size_bytes_tag 2 +#define meshtastic_Compressed_portnum_tag 1 +#define meshtastic_Compressed_data_tag 2 +#define meshtastic_Neighbor_node_id_tag 1 +#define meshtastic_Neighbor_snr_tag 2 +#define meshtastic_Neighbor_last_rx_time_tag 3 +#define meshtastic_Neighbor_node_broadcast_interval_secs_tag 4 +#define meshtastic_NeighborInfo_node_id_tag 1 +#define meshtastic_NeighborInfo_last_sent_by_id_tag 2 +#define meshtastic_NeighborInfo_node_broadcast_interval_secs_tag 3 +#define meshtastic_NeighborInfo_neighbors_tag 4 +#define meshtastic_DeviceMetadata_firmware_version_tag 1 +#define meshtastic_DeviceMetadata_device_state_version_tag 2 +#define meshtastic_DeviceMetadata_canShutdown_tag 3 +#define meshtastic_DeviceMetadata_hasWifi_tag 4 +#define meshtastic_DeviceMetadata_hasBluetooth_tag 5 +#define meshtastic_DeviceMetadata_hasEthernet_tag 6 +#define meshtastic_DeviceMetadata_role_tag 7 +#define meshtastic_DeviceMetadata_position_flags_tag 8 +#define meshtastic_DeviceMetadata_hw_model_tag 9 +#define meshtastic_DeviceMetadata_hasRemoteHardware_tag 10 +#define meshtastic_DeviceMetadata_hasPKC_tag 11 +#define meshtastic_FromRadio_id_tag 1 +#define meshtastic_FromRadio_packet_tag 2 +#define meshtastic_FromRadio_my_info_tag 3 +#define meshtastic_FromRadio_node_info_tag 4 +#define meshtastic_FromRadio_config_tag 5 +#define meshtastic_FromRadio_log_record_tag 6 +#define meshtastic_FromRadio_config_complete_id_tag 7 +#define meshtastic_FromRadio_rebooted_tag 8 +#define meshtastic_FromRadio_moduleConfig_tag 9 +#define meshtastic_FromRadio_channel_tag 10 +#define meshtastic_FromRadio_queueStatus_tag 11 +#define meshtastic_FromRadio_xmodemPacket_tag 12 +#define meshtastic_FromRadio_metadata_tag 13 +#define meshtastic_FromRadio_mqttClientProxyMessage_tag 14 +#define meshtastic_FromRadio_fileInfo_tag 15 +#define meshtastic_FromRadio_clientNotification_tag 16 +#define meshtastic_FromRadio_deviceuiConfig_tag 17 +#define meshtastic_ToRadio_packet_tag 1 +#define meshtastic_ToRadio_want_config_id_tag 3 +#define meshtastic_ToRadio_disconnect_tag 4 +#define meshtastic_ToRadio_xmodemPacket_tag 5 +#define meshtastic_ToRadio_mqttClientProxyMessage_tag 6 +#define meshtastic_ToRadio_heartbeat_tag 7 +#define meshtastic_NodeRemoteHardwarePin_node_num_tag 1 +#define meshtastic_NodeRemoteHardwarePin_pin_tag 2 +#define meshtastic_ChunkedPayload_payload_id_tag 1 +#define meshtastic_ChunkedPayload_chunk_count_tag 2 +#define meshtastic_ChunkedPayload_chunk_index_tag 3 +#define meshtastic_ChunkedPayload_payload_chunk_tag 4 +#define meshtastic_resend_chunks_chunks_tag 1 +#define meshtastic_ChunkedPayloadResponse_payload_id_tag 1 +#define meshtastic_ChunkedPayloadResponse_request_transfer_tag 2 +#define meshtastic_ChunkedPayloadResponse_accept_transfer_tag 3 +#define meshtastic_ChunkedPayloadResponse_resend_chunks_tag 4 + +/* Struct field encoding specification for nanopb */ +#define meshtastic_Position_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, SFIXED32, latitude_i, 1) \ +X(a, STATIC, OPTIONAL, SFIXED32, longitude_i, 2) \ +X(a, STATIC, OPTIONAL, INT32, altitude, 3) \ +X(a, STATIC, SINGULAR, FIXED32, time, 4) \ +X(a, STATIC, SINGULAR, UENUM, location_source, 5) \ +X(a, STATIC, SINGULAR, UENUM, altitude_source, 6) \ +X(a, STATIC, SINGULAR, FIXED32, timestamp, 7) \ +X(a, STATIC, SINGULAR, INT32, timestamp_millis_adjust, 8) \ +X(a, STATIC, OPTIONAL, SINT32, altitude_hae, 9) \ +X(a, STATIC, OPTIONAL, SINT32, altitude_geoidal_separation, 10) \ +X(a, STATIC, SINGULAR, UINT32, PDOP, 11) \ +X(a, STATIC, SINGULAR, UINT32, HDOP, 12) \ +X(a, STATIC, SINGULAR, UINT32, VDOP, 13) \ +X(a, STATIC, SINGULAR, UINT32, gps_accuracy, 14) \ +X(a, STATIC, OPTIONAL, UINT32, ground_speed, 15) \ +X(a, STATIC, OPTIONAL, UINT32, ground_track, 16) \ +X(a, STATIC, SINGULAR, UINT32, fix_quality, 17) \ +X(a, STATIC, SINGULAR, UINT32, fix_type, 18) \ +X(a, STATIC, SINGULAR, UINT32, sats_in_view, 19) \ +X(a, STATIC, SINGULAR, UINT32, sensor_id, 20) \ +X(a, STATIC, SINGULAR, UINT32, next_update, 21) \ +X(a, STATIC, SINGULAR, UINT32, seq_number, 22) \ +X(a, STATIC, SINGULAR, UINT32, precision_bits, 23) +#define meshtastic_Position_CALLBACK NULL +#define meshtastic_Position_DEFAULT NULL + +#define meshtastic_User_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, STRING, id, 1) \ +X(a, STATIC, SINGULAR, STRING, long_name, 2) \ +X(a, STATIC, SINGULAR, STRING, short_name, 3) \ +X(a, STATIC, SINGULAR, FIXED_LENGTH_BYTES, macaddr, 4) \ +X(a, STATIC, SINGULAR, UENUM, hw_model, 5) \ +X(a, STATIC, SINGULAR, BOOL, is_licensed, 6) \ +X(a, STATIC, SINGULAR, UENUM, role, 7) \ +X(a, STATIC, SINGULAR, BYTES, public_key, 8) +#define meshtastic_User_CALLBACK NULL +#define meshtastic_User_DEFAULT NULL + +#define meshtastic_RouteDiscovery_FIELDLIST(X, a) \ +X(a, STATIC, REPEATED, FIXED32, route, 1) \ +X(a, STATIC, REPEATED, INT32, snr_towards, 2) \ +X(a, STATIC, REPEATED, FIXED32, route_back, 3) \ +X(a, STATIC, REPEATED, INT32, snr_back, 4) +#define meshtastic_RouteDiscovery_CALLBACK NULL +#define meshtastic_RouteDiscovery_DEFAULT NULL + +#define meshtastic_Routing_FIELDLIST(X, a) \ +X(a, STATIC, ONEOF, MESSAGE, (variant,route_request,route_request), 1) \ +X(a, STATIC, ONEOF, MESSAGE, (variant,route_reply,route_reply), 2) \ +X(a, STATIC, ONEOF, UENUM, (variant,error_reason,error_reason), 3) +#define meshtastic_Routing_CALLBACK NULL +#define meshtastic_Routing_DEFAULT NULL +#define meshtastic_Routing_variant_route_request_MSGTYPE meshtastic_RouteDiscovery +#define meshtastic_Routing_variant_route_reply_MSGTYPE meshtastic_RouteDiscovery + +#define meshtastic_Data_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, portnum, 1) \ +X(a, STATIC, SINGULAR, BYTES, payload, 2) \ +X(a, STATIC, SINGULAR, BOOL, want_response, 3) \ +X(a, STATIC, SINGULAR, FIXED32, dest, 4) \ +X(a, STATIC, SINGULAR, FIXED32, source, 5) \ +X(a, STATIC, SINGULAR, FIXED32, request_id, 6) \ +X(a, STATIC, SINGULAR, FIXED32, reply_id, 7) \ +X(a, STATIC, SINGULAR, FIXED32, emoji, 8) \ +X(a, STATIC, OPTIONAL, UINT32, bitfield, 9) +#define meshtastic_Data_CALLBACK NULL +#define meshtastic_Data_DEFAULT NULL + +#define meshtastic_Waypoint_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, id, 1) \ +X(a, STATIC, OPTIONAL, SFIXED32, latitude_i, 2) \ +X(a, STATIC, OPTIONAL, SFIXED32, longitude_i, 3) \ +X(a, STATIC, SINGULAR, UINT32, expire, 4) \ +X(a, STATIC, SINGULAR, UINT32, locked_to, 5) \ +X(a, STATIC, SINGULAR, STRING, name, 6) \ +X(a, STATIC, SINGULAR, STRING, description, 7) \ +X(a, STATIC, SINGULAR, FIXED32, icon, 8) +#define meshtastic_Waypoint_CALLBACK NULL +#define meshtastic_Waypoint_DEFAULT NULL + +#define meshtastic_MqttClientProxyMessage_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, STRING, topic, 1) \ +X(a, STATIC, ONEOF, BYTES, (payload_variant,data,payload_variant.data), 2) \ +X(a, STATIC, ONEOF, STRING, (payload_variant,text,payload_variant.text), 3) \ +X(a, STATIC, SINGULAR, BOOL, retained, 4) +#define meshtastic_MqttClientProxyMessage_CALLBACK NULL +#define meshtastic_MqttClientProxyMessage_DEFAULT NULL + +#define meshtastic_MeshPacket_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, FIXED32, from, 1) \ +X(a, STATIC, SINGULAR, FIXED32, to, 2) \ +X(a, STATIC, SINGULAR, UINT32, channel, 3) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,decoded,decoded), 4) \ +X(a, STATIC, ONEOF, BYTES, (payload_variant,encrypted,encrypted), 5) \ +X(a, STATIC, SINGULAR, FIXED32, id, 6) \ +X(a, STATIC, SINGULAR, FIXED32, rx_time, 7) \ +X(a, STATIC, SINGULAR, FLOAT, rx_snr, 8) \ +X(a, STATIC, SINGULAR, UINT32, hop_limit, 9) \ +X(a, STATIC, SINGULAR, BOOL, want_ack, 10) \ +X(a, STATIC, SINGULAR, UENUM, priority, 11) \ +X(a, STATIC, SINGULAR, INT32, rx_rssi, 12) \ +X(a, STATIC, SINGULAR, UENUM, delayed, 13) \ +X(a, STATIC, SINGULAR, BOOL, via_mqtt, 14) \ +X(a, STATIC, SINGULAR, UINT32, hop_start, 15) \ +X(a, STATIC, SINGULAR, BYTES, public_key, 16) \ +X(a, STATIC, SINGULAR, BOOL, pki_encrypted, 17) +#define meshtastic_MeshPacket_CALLBACK NULL +#define meshtastic_MeshPacket_DEFAULT NULL +#define meshtastic_MeshPacket_payload_variant_decoded_MSGTYPE meshtastic_Data + +#define meshtastic_NodeInfo_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, num, 1) \ +X(a, STATIC, OPTIONAL, MESSAGE, user, 2) \ +X(a, STATIC, OPTIONAL, MESSAGE, position, 3) \ +X(a, STATIC, SINGULAR, FLOAT, snr, 4) \ +X(a, STATIC, SINGULAR, FIXED32, last_heard, 5) \ +X(a, STATIC, OPTIONAL, MESSAGE, device_metrics, 6) \ +X(a, STATIC, SINGULAR, UINT32, channel, 7) \ +X(a, STATIC, SINGULAR, BOOL, via_mqtt, 8) \ +X(a, STATIC, OPTIONAL, UINT32, hops_away, 9) \ +X(a, STATIC, SINGULAR, BOOL, is_favorite, 10) +#define meshtastic_NodeInfo_CALLBACK NULL +#define meshtastic_NodeInfo_DEFAULT NULL +#define meshtastic_NodeInfo_user_MSGTYPE meshtastic_User +#define meshtastic_NodeInfo_position_MSGTYPE meshtastic_Position +#define meshtastic_NodeInfo_device_metrics_MSGTYPE meshtastic_DeviceMetrics + +#define meshtastic_MyNodeInfo_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, my_node_num, 1) \ +X(a, STATIC, SINGULAR, UINT32, reboot_count, 8) \ +X(a, STATIC, SINGULAR, UINT32, min_app_version, 11) \ +X(a, STATIC, SINGULAR, BYTES, device_id, 12) +#define meshtastic_MyNodeInfo_CALLBACK NULL +#define meshtastic_MyNodeInfo_DEFAULT NULL + +#define meshtastic_LogRecord_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, STRING, message, 1) \ +X(a, STATIC, SINGULAR, FIXED32, time, 2) \ +X(a, STATIC, SINGULAR, STRING, source, 3) \ +X(a, STATIC, SINGULAR, UENUM, level, 4) +#define meshtastic_LogRecord_CALLBACK NULL +#define meshtastic_LogRecord_DEFAULT NULL + +#define meshtastic_QueueStatus_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, INT32, res, 1) \ +X(a, STATIC, SINGULAR, UINT32, free, 2) \ +X(a, STATIC, SINGULAR, UINT32, maxlen, 3) \ +X(a, STATIC, SINGULAR, UINT32, mesh_packet_id, 4) +#define meshtastic_QueueStatus_CALLBACK NULL +#define meshtastic_QueueStatus_DEFAULT NULL + +#define meshtastic_FromRadio_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, id, 1) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,packet,packet), 2) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,my_info,my_info), 3) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,node_info,node_info), 4) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,config,config), 5) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,log_record,log_record), 6) \ +X(a, STATIC, ONEOF, UINT32, (payload_variant,config_complete_id,config_complete_id), 7) \ +X(a, STATIC, ONEOF, BOOL, (payload_variant,rebooted,rebooted), 8) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,moduleConfig,moduleConfig), 9) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,channel,channel), 10) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,queueStatus,queueStatus), 11) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,xmodemPacket,xmodemPacket), 12) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,metadata,metadata), 13) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,mqttClientProxyMessage,mqttClientProxyMessage), 14) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,fileInfo,fileInfo), 15) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,clientNotification,clientNotification), 16) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,deviceuiConfig,deviceuiConfig), 17) +#define meshtastic_FromRadio_CALLBACK NULL +#define meshtastic_FromRadio_DEFAULT NULL +#define meshtastic_FromRadio_payload_variant_packet_MSGTYPE meshtastic_MeshPacket +#define meshtastic_FromRadio_payload_variant_my_info_MSGTYPE meshtastic_MyNodeInfo +#define meshtastic_FromRadio_payload_variant_node_info_MSGTYPE meshtastic_NodeInfo +#define meshtastic_FromRadio_payload_variant_config_MSGTYPE meshtastic_Config +#define meshtastic_FromRadio_payload_variant_log_record_MSGTYPE meshtastic_LogRecord +#define meshtastic_FromRadio_payload_variant_moduleConfig_MSGTYPE meshtastic_ModuleConfig +#define meshtastic_FromRadio_payload_variant_channel_MSGTYPE meshtastic_Channel +#define meshtastic_FromRadio_payload_variant_queueStatus_MSGTYPE meshtastic_QueueStatus +#define meshtastic_FromRadio_payload_variant_xmodemPacket_MSGTYPE meshtastic_XModem +#define meshtastic_FromRadio_payload_variant_metadata_MSGTYPE meshtastic_DeviceMetadata +#define meshtastic_FromRadio_payload_variant_mqttClientProxyMessage_MSGTYPE meshtastic_MqttClientProxyMessage +#define meshtastic_FromRadio_payload_variant_fileInfo_MSGTYPE meshtastic_FileInfo +#define meshtastic_FromRadio_payload_variant_clientNotification_MSGTYPE meshtastic_ClientNotification +#define meshtastic_FromRadio_payload_variant_deviceuiConfig_MSGTYPE meshtastic_DeviceUIConfig + +#define meshtastic_ClientNotification_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, UINT32, reply_id, 1) \ +X(a, STATIC, SINGULAR, FIXED32, time, 2) \ +X(a, STATIC, SINGULAR, UENUM, level, 3) \ +X(a, STATIC, SINGULAR, STRING, message, 4) +#define meshtastic_ClientNotification_CALLBACK NULL +#define meshtastic_ClientNotification_DEFAULT NULL + +#define meshtastic_FileInfo_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, STRING, file_name, 1) \ +X(a, STATIC, SINGULAR, UINT32, size_bytes, 2) +#define meshtastic_FileInfo_CALLBACK NULL +#define meshtastic_FileInfo_DEFAULT NULL + +#define meshtastic_ToRadio_FIELDLIST(X, a) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,packet,packet), 1) \ +X(a, STATIC, ONEOF, UINT32, (payload_variant,want_config_id,want_config_id), 3) \ +X(a, STATIC, ONEOF, BOOL, (payload_variant,disconnect,disconnect), 4) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,xmodemPacket,xmodemPacket), 5) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,mqttClientProxyMessage,mqttClientProxyMessage), 6) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,heartbeat,heartbeat), 7) +#define meshtastic_ToRadio_CALLBACK NULL +#define meshtastic_ToRadio_DEFAULT NULL +#define meshtastic_ToRadio_payload_variant_packet_MSGTYPE meshtastic_MeshPacket +#define meshtastic_ToRadio_payload_variant_xmodemPacket_MSGTYPE meshtastic_XModem +#define meshtastic_ToRadio_payload_variant_mqttClientProxyMessage_MSGTYPE meshtastic_MqttClientProxyMessage +#define meshtastic_ToRadio_payload_variant_heartbeat_MSGTYPE meshtastic_Heartbeat + +#define meshtastic_Compressed_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, portnum, 1) \ +X(a, STATIC, SINGULAR, BYTES, data, 2) +#define meshtastic_Compressed_CALLBACK NULL +#define meshtastic_Compressed_DEFAULT NULL + +#define meshtastic_NeighborInfo_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, node_id, 1) \ +X(a, STATIC, SINGULAR, UINT32, last_sent_by_id, 2) \ +X(a, STATIC, SINGULAR, UINT32, node_broadcast_interval_secs, 3) \ +X(a, STATIC, REPEATED, MESSAGE, neighbors, 4) +#define meshtastic_NeighborInfo_CALLBACK NULL +#define meshtastic_NeighborInfo_DEFAULT NULL +#define meshtastic_NeighborInfo_neighbors_MSGTYPE meshtastic_Neighbor + +#define meshtastic_Neighbor_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, node_id, 1) \ +X(a, STATIC, SINGULAR, FLOAT, snr, 2) \ +X(a, STATIC, SINGULAR, FIXED32, last_rx_time, 3) \ +X(a, STATIC, SINGULAR, UINT32, node_broadcast_interval_secs, 4) +#define meshtastic_Neighbor_CALLBACK NULL +#define meshtastic_Neighbor_DEFAULT NULL + +#define meshtastic_DeviceMetadata_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, STRING, firmware_version, 1) \ +X(a, STATIC, SINGULAR, UINT32, device_state_version, 2) \ +X(a, STATIC, SINGULAR, BOOL, canShutdown, 3) \ +X(a, STATIC, SINGULAR, BOOL, hasWifi, 4) \ +X(a, STATIC, SINGULAR, BOOL, hasBluetooth, 5) \ +X(a, STATIC, SINGULAR, BOOL, hasEthernet, 6) \ +X(a, STATIC, SINGULAR, UENUM, role, 7) \ +X(a, STATIC, SINGULAR, UINT32, position_flags, 8) \ +X(a, STATIC, SINGULAR, UENUM, hw_model, 9) \ +X(a, STATIC, SINGULAR, BOOL, hasRemoteHardware, 10) \ +X(a, STATIC, SINGULAR, BOOL, hasPKC, 11) +#define meshtastic_DeviceMetadata_CALLBACK NULL +#define meshtastic_DeviceMetadata_DEFAULT NULL + +#define meshtastic_Heartbeat_FIELDLIST(X, a) \ + +#define meshtastic_Heartbeat_CALLBACK NULL +#define meshtastic_Heartbeat_DEFAULT NULL + +#define meshtastic_NodeRemoteHardwarePin_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, node_num, 1) \ +X(a, STATIC, OPTIONAL, MESSAGE, pin, 2) +#define meshtastic_NodeRemoteHardwarePin_CALLBACK NULL +#define meshtastic_NodeRemoteHardwarePin_DEFAULT NULL +#define meshtastic_NodeRemoteHardwarePin_pin_MSGTYPE meshtastic_RemoteHardwarePin + +#define meshtastic_ChunkedPayload_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, payload_id, 1) \ +X(a, STATIC, SINGULAR, UINT32, chunk_count, 2) \ +X(a, STATIC, SINGULAR, UINT32, chunk_index, 3) \ +X(a, STATIC, SINGULAR, BYTES, payload_chunk, 4) +#define meshtastic_ChunkedPayload_CALLBACK NULL +#define meshtastic_ChunkedPayload_DEFAULT NULL + +#define meshtastic_resend_chunks_FIELDLIST(X, a) \ +X(a, CALLBACK, REPEATED, UINT32, chunks, 1) +#define meshtastic_resend_chunks_CALLBACK pb_default_field_callback +#define meshtastic_resend_chunks_DEFAULT NULL + +#define meshtastic_ChunkedPayloadResponse_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, payload_id, 1) \ +X(a, STATIC, ONEOF, BOOL, (payload_variant,request_transfer,payload_variant.request_transfer), 2) \ +X(a, STATIC, ONEOF, BOOL, (payload_variant,accept_transfer,payload_variant.accept_transfer), 3) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,resend_chunks,payload_variant.resend_chunks), 4) +#define meshtastic_ChunkedPayloadResponse_CALLBACK NULL +#define meshtastic_ChunkedPayloadResponse_DEFAULT NULL +#define meshtastic_ChunkedPayloadResponse_payload_variant_resend_chunks_MSGTYPE meshtastic_resend_chunks + +extern const pb_msgdesc_t meshtastic_Position_msg; +extern const pb_msgdesc_t meshtastic_User_msg; +extern const pb_msgdesc_t meshtastic_RouteDiscovery_msg; +extern const pb_msgdesc_t meshtastic_Routing_msg; +extern const pb_msgdesc_t meshtastic_Data_msg; +extern const pb_msgdesc_t meshtastic_Waypoint_msg; +extern const pb_msgdesc_t meshtastic_MqttClientProxyMessage_msg; +extern const pb_msgdesc_t meshtastic_MeshPacket_msg; +extern const pb_msgdesc_t meshtastic_NodeInfo_msg; +extern const pb_msgdesc_t meshtastic_MyNodeInfo_msg; +extern const pb_msgdesc_t meshtastic_LogRecord_msg; +extern const pb_msgdesc_t meshtastic_QueueStatus_msg; +extern const pb_msgdesc_t meshtastic_FromRadio_msg; +extern const pb_msgdesc_t meshtastic_ClientNotification_msg; +extern const pb_msgdesc_t meshtastic_FileInfo_msg; +extern const pb_msgdesc_t meshtastic_ToRadio_msg; +extern const pb_msgdesc_t meshtastic_Compressed_msg; +extern const pb_msgdesc_t meshtastic_NeighborInfo_msg; +extern const pb_msgdesc_t meshtastic_Neighbor_msg; +extern const pb_msgdesc_t meshtastic_DeviceMetadata_msg; +extern const pb_msgdesc_t meshtastic_Heartbeat_msg; +extern const pb_msgdesc_t meshtastic_NodeRemoteHardwarePin_msg; +extern const pb_msgdesc_t meshtastic_ChunkedPayload_msg; +extern const pb_msgdesc_t meshtastic_resend_chunks_msg; +extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define meshtastic_Position_fields &meshtastic_Position_msg +#define meshtastic_User_fields &meshtastic_User_msg +#define meshtastic_RouteDiscovery_fields &meshtastic_RouteDiscovery_msg +#define meshtastic_Routing_fields &meshtastic_Routing_msg +#define meshtastic_Data_fields &meshtastic_Data_msg +#define meshtastic_Waypoint_fields &meshtastic_Waypoint_msg +#define meshtastic_MqttClientProxyMessage_fields &meshtastic_MqttClientProxyMessage_msg +#define meshtastic_MeshPacket_fields &meshtastic_MeshPacket_msg +#define meshtastic_NodeInfo_fields &meshtastic_NodeInfo_msg +#define meshtastic_MyNodeInfo_fields &meshtastic_MyNodeInfo_msg +#define meshtastic_LogRecord_fields &meshtastic_LogRecord_msg +#define meshtastic_QueueStatus_fields &meshtastic_QueueStatus_msg +#define meshtastic_FromRadio_fields &meshtastic_FromRadio_msg +#define meshtastic_ClientNotification_fields &meshtastic_ClientNotification_msg +#define meshtastic_FileInfo_fields &meshtastic_FileInfo_msg +#define meshtastic_ToRadio_fields &meshtastic_ToRadio_msg +#define meshtastic_Compressed_fields &meshtastic_Compressed_msg +#define meshtastic_NeighborInfo_fields &meshtastic_NeighborInfo_msg +#define meshtastic_Neighbor_fields &meshtastic_Neighbor_msg +#define meshtastic_DeviceMetadata_fields &meshtastic_DeviceMetadata_msg +#define meshtastic_Heartbeat_fields &meshtastic_Heartbeat_msg +#define meshtastic_NodeRemoteHardwarePin_fields &meshtastic_NodeRemoteHardwarePin_msg +#define meshtastic_ChunkedPayload_fields &meshtastic_ChunkedPayload_msg +#define meshtastic_resend_chunks_fields &meshtastic_resend_chunks_msg +#define meshtastic_ChunkedPayloadResponse_fields &meshtastic_ChunkedPayloadResponse_msg + +/* Maximum encoded size of messages (where known) */ +/* meshtastic_resend_chunks_size depends on runtime parameters */ +/* meshtastic_ChunkedPayloadResponse_size depends on runtime parameters */ +#define MESHTASTIC_MESHTASTIC_MESH_PB_H_MAX_SIZE meshtastic_FromRadio_size +#define meshtastic_ChunkedPayload_size 245 +#define meshtastic_ClientNotification_size 415 +#define meshtastic_Compressed_size 243 +#define meshtastic_Data_size 273 +#define meshtastic_DeviceMetadata_size 48 +#define meshtastic_FileInfo_size 236 +#define meshtastic_FromRadio_size 510 +#define meshtastic_Heartbeat_size 0 +#define meshtastic_LogRecord_size 426 +#define meshtastic_MeshPacket_size 367 +#define meshtastic_MqttClientProxyMessage_size 501 +#define meshtastic_MyNodeInfo_size 36 +#define meshtastic_NeighborInfo_size 258 +#define meshtastic_Neighbor_size 22 +#define meshtastic_NodeInfo_size 317 +#define meshtastic_NodeRemoteHardwarePin_size 29 +#define meshtastic_Position_size 144 +#define meshtastic_QueueStatus_size 23 +#define meshtastic_RouteDiscovery_size 256 +#define meshtastic_Routing_size 259 +#define meshtastic_ToRadio_size 504 +#define meshtastic_User_size 113 +#define meshtastic_Waypoint_size 165 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/mesh/generated/meshtastic/module_config.pb.cpp b/src/mesh/generated/meshtastic/module_config.pb.cpp new file mode 100644 index 0000000..c40041e --- /dev/null +++ b/src/mesh/generated/meshtastic/module_config.pb.cpp @@ -0,0 +1,69 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9 */ + +#include "meshtastic/module_config.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(meshtastic_ModuleConfig, meshtastic_ModuleConfig, 2) + + +PB_BIND(meshtastic_ModuleConfig_MQTTConfig, meshtastic_ModuleConfig_MQTTConfig, 2) + + +PB_BIND(meshtastic_ModuleConfig_MapReportSettings, meshtastic_ModuleConfig_MapReportSettings, AUTO) + + +PB_BIND(meshtastic_ModuleConfig_RemoteHardwareConfig, meshtastic_ModuleConfig_RemoteHardwareConfig, AUTO) + + +PB_BIND(meshtastic_ModuleConfig_NeighborInfoConfig, meshtastic_ModuleConfig_NeighborInfoConfig, AUTO) + + +PB_BIND(meshtastic_ModuleConfig_DetectionSensorConfig, meshtastic_ModuleConfig_DetectionSensorConfig, AUTO) + + +PB_BIND(meshtastic_ModuleConfig_AudioConfig, meshtastic_ModuleConfig_AudioConfig, AUTO) + + +PB_BIND(meshtastic_ModuleConfig_PaxcounterConfig, meshtastic_ModuleConfig_PaxcounterConfig, AUTO) + + +PB_BIND(meshtastic_ModuleConfig_SerialConfig, meshtastic_ModuleConfig_SerialConfig, AUTO) + + +PB_BIND(meshtastic_ModuleConfig_ExternalNotificationConfig, meshtastic_ModuleConfig_ExternalNotificationConfig, AUTO) + + +PB_BIND(meshtastic_ModuleConfig_StoreForwardConfig, meshtastic_ModuleConfig_StoreForwardConfig, AUTO) + + +PB_BIND(meshtastic_ModuleConfig_RangeTestConfig, meshtastic_ModuleConfig_RangeTestConfig, AUTO) + + +PB_BIND(meshtastic_ModuleConfig_TelemetryConfig, meshtastic_ModuleConfig_TelemetryConfig, AUTO) + + +PB_BIND(meshtastic_ModuleConfig_CannedMessageConfig, meshtastic_ModuleConfig_CannedMessageConfig, AUTO) + + +PB_BIND(meshtastic_ModuleConfig_AmbientLightingConfig, meshtastic_ModuleConfig_AmbientLightingConfig, AUTO) + + +PB_BIND(meshtastic_RemoteHardwarePin, meshtastic_RemoteHardwarePin, AUTO) + + + + + + + + + + + + + + + diff --git a/src/mesh/generated/meshtastic/module_config.pb.h b/src/mesh/generated/meshtastic/module_config.pb.h new file mode 100644 index 0000000..32d5ded --- /dev/null +++ b/src/mesh/generated/meshtastic/module_config.pb.h @@ -0,0 +1,901 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9 */ + +#ifndef PB_MESHTASTIC_MESHTASTIC_MODULE_CONFIG_PB_H_INCLUDED +#define PB_MESHTASTIC_MESHTASTIC_MODULE_CONFIG_PB_H_INCLUDED +#include + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Enum definitions */ +typedef enum _meshtastic_RemoteHardwarePinType { + /* Unset/unused */ + meshtastic_RemoteHardwarePinType_UNKNOWN = 0, + /* GPIO pin can be read (if it is high / low) */ + meshtastic_RemoteHardwarePinType_DIGITAL_READ = 1, + /* GPIO pin can be written to (high / low) */ + meshtastic_RemoteHardwarePinType_DIGITAL_WRITE = 2 +} meshtastic_RemoteHardwarePinType; + +typedef enum _meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType { + /* Event is triggered if pin is low */ + meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_LOW = 0, + /* Event is triggered if pin is high */ + meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_HIGH = 1, + /* Event is triggered when pin goes high to low */ + meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_FALLING_EDGE = 2, + /* Event is triggered when pin goes low to high */ + meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_RISING_EDGE = 3, + /* Event is triggered on every pin state change, low is considered to be + "active" */ + meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_EITHER_EDGE_ACTIVE_LOW = 4, + /* Event is triggered on every pin state change, high is considered to be + "active" */ + meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_EITHER_EDGE_ACTIVE_HIGH = 5 +} meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType; + +/* Baudrate for codec2 voice */ +typedef enum _meshtastic_ModuleConfig_AudioConfig_Audio_Baud { + meshtastic_ModuleConfig_AudioConfig_Audio_Baud_CODEC2_DEFAULT = 0, + meshtastic_ModuleConfig_AudioConfig_Audio_Baud_CODEC2_3200 = 1, + meshtastic_ModuleConfig_AudioConfig_Audio_Baud_CODEC2_2400 = 2, + meshtastic_ModuleConfig_AudioConfig_Audio_Baud_CODEC2_1600 = 3, + meshtastic_ModuleConfig_AudioConfig_Audio_Baud_CODEC2_1400 = 4, + meshtastic_ModuleConfig_AudioConfig_Audio_Baud_CODEC2_1300 = 5, + meshtastic_ModuleConfig_AudioConfig_Audio_Baud_CODEC2_1200 = 6, + meshtastic_ModuleConfig_AudioConfig_Audio_Baud_CODEC2_700 = 7, + meshtastic_ModuleConfig_AudioConfig_Audio_Baud_CODEC2_700B = 8 +} meshtastic_ModuleConfig_AudioConfig_Audio_Baud; + +/* TODO: REPLACE */ +typedef enum _meshtastic_ModuleConfig_SerialConfig_Serial_Baud { + meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_DEFAULT = 0, + meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_110 = 1, + meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_300 = 2, + meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_600 = 3, + meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_1200 = 4, + meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_2400 = 5, + meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_4800 = 6, + meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_9600 = 7, + meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_19200 = 8, + meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_38400 = 9, + meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_57600 = 10, + meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_115200 = 11, + meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_230400 = 12, + meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_460800 = 13, + meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_576000 = 14, + meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_921600 = 15 +} meshtastic_ModuleConfig_SerialConfig_Serial_Baud; + +/* TODO: REPLACE */ +typedef enum _meshtastic_ModuleConfig_SerialConfig_Serial_Mode { + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT = 0, + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE = 1, + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO = 2, + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG = 3, + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA = 4, + /* NMEA messages specifically tailored for CalTopo */ + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO = 5, + /* Ecowitt WS85 weather station */ + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85 = 6 +} meshtastic_ModuleConfig_SerialConfig_Serial_Mode; + +/* TODO: REPLACE */ +typedef enum _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar { + /* TODO: REPLACE */ + meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE = 0, + /* TODO: REPLACE */ + meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP = 17, + /* TODO: REPLACE */ + meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN = 18, + /* TODO: REPLACE */ + meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT = 19, + /* TODO: REPLACE */ + meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT = 20, + /* '\n' */ + meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT = 10, + /* TODO: REPLACE */ + meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK = 27, + /* TODO: REPLACE */ + meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL = 24 +} meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar; + +/* Struct definitions */ +/* Settings for reporting unencrypted information about our node to a map via MQTT */ +typedef struct _meshtastic_ModuleConfig_MapReportSettings { + /* How often we should report our info to the map (in seconds) */ + uint32_t publish_interval_secs; + /* Bits of precision for the location sent (default of 32 is full precision). */ + uint32_t position_precision; +} meshtastic_ModuleConfig_MapReportSettings; + +/* MQTT Client Config */ +typedef struct _meshtastic_ModuleConfig_MQTTConfig { + /* If a meshtastic node is able to reach the internet it will normally attempt to gateway any channels that are marked as + is_uplink_enabled or is_downlink_enabled. */ + bool enabled; + /* The server to use for our MQTT global message gateway feature. + If not set, the default server will be used */ + char address[64]; + /* MQTT username to use (most useful for a custom MQTT server). + If using a custom server, this will be honoured even if empty. + If using the default server, this will only be honoured if set, otherwise the device will use the default username */ + char username[64]; + /* MQTT password to use (most useful for a custom MQTT server). + If using a custom server, this will be honoured even if empty. + If using the default server, this will only be honoured if set, otherwise the device will use the default password */ + char password[64]; + /* Whether to send encrypted or decrypted packets to MQTT. + This parameter is only honoured if you also set server + (the default official mqtt.meshtastic.org server can handle encrypted packets) + Decrypted packets may be useful for external systems that want to consume meshtastic packets */ + bool encryption_enabled; + /* Whether to send / consume json packets on MQTT */ + bool json_enabled; + /* If true, we attempt to establish a secure connection using TLS */ + bool tls_enabled; + /* The root topic to use for MQTT messages. Default is "msh". + This is useful if you want to use a single MQTT server for multiple meshtastic networks and separate them via ACLs */ + char root[32]; + /* If true, we can use the connected phone / client to proxy messages to MQTT instead of a direct connection */ + bool proxy_to_client_enabled; + /* If true, we will periodically report unencrypted information about our node to a map via MQTT */ + bool map_reporting_enabled; + /* Settings for reporting information about our node to a map via MQTT */ + bool has_map_report_settings; + meshtastic_ModuleConfig_MapReportSettings map_report_settings; +} meshtastic_ModuleConfig_MQTTConfig; + +/* NeighborInfoModule Config */ +typedef struct _meshtastic_ModuleConfig_NeighborInfoConfig { + /* Whether the Module is enabled */ + bool enabled; + /* Interval in seconds of how often we should try to send our + Neighbor Info to the mesh */ + uint32_t update_interval; +} meshtastic_ModuleConfig_NeighborInfoConfig; + +/* Detection Sensor Module Config */ +typedef struct _meshtastic_ModuleConfig_DetectionSensorConfig { + /* Whether the Module is enabled */ + bool enabled; + /* Interval in seconds of how often we can send a message to the mesh when a + trigger event is detected */ + uint32_t minimum_broadcast_secs; + /* Interval in seconds of how often we should send a message to the mesh + with the current state regardless of trigger events When set to 0, only + trigger events will be broadcasted Works as a sort of status heartbeat + for peace of mind */ + uint32_t state_broadcast_secs; + /* Send ASCII bell with alert message + Useful for triggering ext. notification on bell */ + bool send_bell; + /* Friendly name used to format message sent to mesh + Example: A name "Motion" would result in a message "Motion detected" + Maximum length of 20 characters */ + char name[20]; + /* GPIO pin to monitor for state changes */ + uint8_t monitor_pin; + /* The type of trigger event to be used */ + meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType detection_trigger_type; + /* Whether or not use INPUT_PULLUP mode for GPIO pin + Only applicable if the board uses pull-up resistors on the pin */ + bool use_pullup; +} meshtastic_ModuleConfig_DetectionSensorConfig; + +/* Audio Config for codec2 voice */ +typedef struct _meshtastic_ModuleConfig_AudioConfig { + /* Whether Audio is enabled */ + bool codec2_enabled; + /* PTT Pin */ + uint8_t ptt_pin; + /* The audio sample rate to use for codec2 */ + meshtastic_ModuleConfig_AudioConfig_Audio_Baud bitrate; + /* I2S Word Select */ + uint8_t i2s_ws; + /* I2S Data IN */ + uint8_t i2s_sd; + /* I2S Data OUT */ + uint8_t i2s_din; + /* I2S Clock */ + uint8_t i2s_sck; +} meshtastic_ModuleConfig_AudioConfig; + +/* Config for the Paxcounter Module */ +typedef struct _meshtastic_ModuleConfig_PaxcounterConfig { + /* Enable the Paxcounter Module */ + bool enabled; + uint32_t paxcounter_update_interval; + /* WiFi RSSI threshold. Defaults to -80 */ + int32_t wifi_threshold; + /* BLE RSSI threshold. Defaults to -80 */ + int32_t ble_threshold; +} meshtastic_ModuleConfig_PaxcounterConfig; + +/* Serial Config */ +typedef struct _meshtastic_ModuleConfig_SerialConfig { + /* Preferences for the SerialModule */ + bool enabled; + /* TODO: REPLACE */ + bool echo; + /* RX pin (should match Arduino gpio pin number) */ + uint32_t rxd; + /* TX pin (should match Arduino gpio pin number) */ + uint32_t txd; + /* Serial baud rate */ + meshtastic_ModuleConfig_SerialConfig_Serial_Baud baud; + /* TODO: REPLACE */ + uint32_t timeout; + /* Mode for serial module operation */ + meshtastic_ModuleConfig_SerialConfig_Serial_Mode mode; + /* Overrides the platform's defacto Serial port instance to use with Serial module config settings + This is currently only usable in output modes like NMEA / CalTopo and may behave strangely or not work at all in other modes + Existing logging over the Serial Console will still be present */ + bool override_console_serial_port; +} meshtastic_ModuleConfig_SerialConfig; + +/* External Notifications Config */ +typedef struct _meshtastic_ModuleConfig_ExternalNotificationConfig { + /* Enable the ExternalNotificationModule */ + bool enabled; + /* When using in On/Off mode, keep the output on for this many + milliseconds. Default 1000ms (1 second). */ + uint32_t output_ms; + /* Define the output pin GPIO setting Defaults to + EXT_NOTIFY_OUT if set for the board. + In standalone devices this pin should drive the LED to match the UI. */ + uint32_t output; + /* IF this is true, the 'output' Pin will be pulled active high, false + means active low. */ + bool active; + /* True: Alert when a text message arrives (output) */ + bool alert_message; + /* True: Alert when the bell character is received (output) */ + bool alert_bell; + /* use a PWM output instead of a simple on/off output. This will ignore + the 'output', 'output_ms' and 'active' settings and use the + device.buzzer_gpio instead. */ + bool use_pwm; + /* Optional: Define a secondary output pin for a vibra motor + This is used in standalone devices to match the UI. */ + uint8_t output_vibra; + /* Optional: Define a tertiary output pin for an active buzzer + This is used in standalone devices to to match the UI. */ + uint8_t output_buzzer; + /* True: Alert when a text message arrives (output_vibra) */ + bool alert_message_vibra; + /* True: Alert when a text message arrives (output_buzzer) */ + bool alert_message_buzzer; + /* True: Alert when the bell character is received (output_vibra) */ + bool alert_bell_vibra; + /* True: Alert when the bell character is received (output_buzzer) */ + bool alert_bell_buzzer; + /* The notification will toggle with 'output_ms' for this time of seconds. + Default is 0 which means don't repeat at all. 60 would mean blink + and/or beep for 60 seconds */ + uint16_t nag_timeout; + /* When true, enables devices with native I2S audio output to use the RTTTL over speaker like a buzzer + T-Watch S3 and T-Deck for example have this capability */ + bool use_i2s_as_buzzer; +} meshtastic_ModuleConfig_ExternalNotificationConfig; + +/* Store and Forward Module Config */ +typedef struct _meshtastic_ModuleConfig_StoreForwardConfig { + /* Enable the Store and Forward Module */ + bool enabled; + /* TODO: REPLACE */ + bool heartbeat; + /* TODO: REPLACE */ + uint32_t records; + /* TODO: REPLACE */ + uint32_t history_return_max; + /* TODO: REPLACE */ + uint32_t history_return_window; + /* Set to true to let this node act as a server that stores received messages and resends them upon request. */ + bool is_server; +} meshtastic_ModuleConfig_StoreForwardConfig; + +/* Preferences for the RangeTestModule */ +typedef struct _meshtastic_ModuleConfig_RangeTestConfig { + /* Enable the Range Test Module */ + bool enabled; + /* Send out range test messages from this node */ + uint32_t sender; + /* Bool value indicating that this node should save a RangeTest.csv file. + ESP32 Only */ + bool save; +} meshtastic_ModuleConfig_RangeTestConfig; + +/* Configuration for both device and environment metrics */ +typedef struct _meshtastic_ModuleConfig_TelemetryConfig { + /* Interval in seconds of how often we should try to send our + device metrics to the mesh */ + uint32_t device_update_interval; + uint32_t environment_update_interval; + /* Preferences for the Telemetry Module (Environment) + Enable/Disable the telemetry measurement module measurement collection */ + bool environment_measurement_enabled; + /* Enable/Disable the telemetry measurement module on-device display */ + bool environment_screen_enabled; + /* We'll always read the sensor in Celsius, but sometimes we might want to + display the results in Fahrenheit as a "user preference". */ + bool environment_display_fahrenheit; + /* Enable/Disable the air quality metrics */ + bool air_quality_enabled; + /* Interval in seconds of how often we should try to send our + air quality metrics to the mesh */ + uint32_t air_quality_interval; + /* Enable/disable Power metrics */ + bool power_measurement_enabled; + /* Interval in seconds of how often we should try to send our + power metrics to the mesh */ + uint32_t power_update_interval; + /* Enable/Disable the power measurement module on-device display */ + bool power_screen_enabled; + /* Preferences for the (Health) Telemetry Module + Enable/Disable the telemetry measurement module measurement collection */ + bool health_measurement_enabled; + /* Interval in seconds of how often we should try to send our + health metrics to the mesh */ + uint32_t health_update_interval; + /* Enable/Disable the health telemetry module on-device display */ + bool health_screen_enabled; +} meshtastic_ModuleConfig_TelemetryConfig; + +/* TODO: REPLACE */ +typedef struct _meshtastic_ModuleConfig_CannedMessageConfig { + /* Enable the rotary encoder #1. This is a 'dumb' encoder sending pulses on both A and B pins while rotating. */ + bool rotary1_enabled; + /* GPIO pin for rotary encoder A port. */ + uint32_t inputbroker_pin_a; + /* GPIO pin for rotary encoder B port. */ + uint32_t inputbroker_pin_b; + /* GPIO pin for rotary encoder Press port. */ + uint32_t inputbroker_pin_press; + /* Generate input event on CW of this kind. */ + meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar inputbroker_event_cw; + /* Generate input event on CCW of this kind. */ + meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar inputbroker_event_ccw; + /* Generate input event on Press of this kind. */ + meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar inputbroker_event_press; + /* Enable the Up/Down/Select input device. Can be RAK rotary encoder or 3 buttons. Uses the a/b/press definitions from inputbroker. */ + bool updown1_enabled; + /* Enable/disable CannedMessageModule. */ + bool enabled; + /* Input event origin accepted by the canned message module. + Can be e.g. "rotEnc1", "upDownEnc1", "scanAndSelect", "cardkb", "serialkb", or keyword "_any" */ + char allow_input_source[16]; + /* CannedMessageModule also sends a bell character with the messages. + ExternalNotificationModule can benefit from this feature. */ + bool send_bell; +} meshtastic_ModuleConfig_CannedMessageConfig; + +/* Ambient Lighting Module - Settings for control of onboard LEDs to allow users to adjust the brightness levels and respective color levels. +Initially created for the RAK14001 RGB LED module. */ +typedef struct _meshtastic_ModuleConfig_AmbientLightingConfig { + /* Sets LED to on or off. */ + bool led_state; + /* Sets the current for the LED output. Default is 10. */ + uint8_t current; + /* Sets the red LED level. Values are 0-255. */ + uint8_t red; + /* Sets the green LED level. Values are 0-255. */ + uint8_t green; + /* Sets the blue LED level. Values are 0-255. */ + uint8_t blue; +} meshtastic_ModuleConfig_AmbientLightingConfig; + +/* A GPIO pin definition for remote hardware module */ +typedef struct _meshtastic_RemoteHardwarePin { + /* GPIO Pin number (must match Arduino) */ + uint8_t gpio_pin; + /* Name for the GPIO pin (i.e. Front gate, mailbox, etc) */ + char name[15]; + /* Type of GPIO access available to consumers on the mesh */ + meshtastic_RemoteHardwarePinType type; +} meshtastic_RemoteHardwarePin; + +/* RemoteHardwareModule Config */ +typedef struct _meshtastic_ModuleConfig_RemoteHardwareConfig { + /* Whether the Module is enabled */ + bool enabled; + /* Whether the Module allows consumers to read / write to pins not defined in available_pins */ + bool allow_undefined_pin_access; + /* Exposes the available pins to the mesh for reading and writing */ + pb_size_t available_pins_count; + meshtastic_RemoteHardwarePin available_pins[4]; +} meshtastic_ModuleConfig_RemoteHardwareConfig; + +/* Module Config */ +typedef struct _meshtastic_ModuleConfig { + pb_size_t which_payload_variant; + union { + /* TODO: REPLACE */ + meshtastic_ModuleConfig_MQTTConfig mqtt; + /* TODO: REPLACE */ + meshtastic_ModuleConfig_SerialConfig serial; + /* TODO: REPLACE */ + meshtastic_ModuleConfig_ExternalNotificationConfig external_notification; + /* TODO: REPLACE */ + meshtastic_ModuleConfig_StoreForwardConfig store_forward; + /* TODO: REPLACE */ + meshtastic_ModuleConfig_RangeTestConfig range_test; + /* TODO: REPLACE */ + meshtastic_ModuleConfig_TelemetryConfig telemetry; + /* TODO: REPLACE */ + meshtastic_ModuleConfig_CannedMessageConfig canned_message; + /* TODO: REPLACE */ + meshtastic_ModuleConfig_AudioConfig audio; + /* TODO: REPLACE */ + meshtastic_ModuleConfig_RemoteHardwareConfig remote_hardware; + /* TODO: REPLACE */ + meshtastic_ModuleConfig_NeighborInfoConfig neighbor_info; + /* TODO: REPLACE */ + meshtastic_ModuleConfig_AmbientLightingConfig ambient_lighting; + /* TODO: REPLACE */ + meshtastic_ModuleConfig_DetectionSensorConfig detection_sensor; + /* TODO: REPLACE */ + meshtastic_ModuleConfig_PaxcounterConfig paxcounter; + } payload_variant; +} meshtastic_ModuleConfig; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Helper constants for enums */ +#define _meshtastic_RemoteHardwarePinType_MIN meshtastic_RemoteHardwarePinType_UNKNOWN +#define _meshtastic_RemoteHardwarePinType_MAX meshtastic_RemoteHardwarePinType_DIGITAL_WRITE +#define _meshtastic_RemoteHardwarePinType_ARRAYSIZE ((meshtastic_RemoteHardwarePinType)(meshtastic_RemoteHardwarePinType_DIGITAL_WRITE+1)) + +#define _meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_MIN meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_LOW +#define _meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_MAX meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_EITHER_EDGE_ACTIVE_HIGH +#define _meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_ARRAYSIZE ((meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType)(meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_EITHER_EDGE_ACTIVE_HIGH+1)) + +#define _meshtastic_ModuleConfig_AudioConfig_Audio_Baud_MIN meshtastic_ModuleConfig_AudioConfig_Audio_Baud_CODEC2_DEFAULT +#define _meshtastic_ModuleConfig_AudioConfig_Audio_Baud_MAX meshtastic_ModuleConfig_AudioConfig_Audio_Baud_CODEC2_700B +#define _meshtastic_ModuleConfig_AudioConfig_Audio_Baud_ARRAYSIZE ((meshtastic_ModuleConfig_AudioConfig_Audio_Baud)(meshtastic_ModuleConfig_AudioConfig_Audio_Baud_CODEC2_700B+1)) + +#define _meshtastic_ModuleConfig_SerialConfig_Serial_Baud_MIN meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_DEFAULT +#define _meshtastic_ModuleConfig_SerialConfig_Serial_Baud_MAX meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_921600 +#define _meshtastic_ModuleConfig_SerialConfig_Serial_Baud_ARRAYSIZE ((meshtastic_ModuleConfig_SerialConfig_Serial_Baud)(meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_921600+1)) + +#define _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MIN meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT +#define _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MAX meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85 +#define _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_ARRAYSIZE ((meshtastic_ModuleConfig_SerialConfig_Serial_Mode)(meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85+1)) + +#define _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE +#define _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MAX meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK +#define _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_ARRAYSIZE ((meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar)(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK+1)) + + + + + + +#define meshtastic_ModuleConfig_DetectionSensorConfig_detection_trigger_type_ENUMTYPE meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType + +#define meshtastic_ModuleConfig_AudioConfig_bitrate_ENUMTYPE meshtastic_ModuleConfig_AudioConfig_Audio_Baud + + +#define meshtastic_ModuleConfig_SerialConfig_baud_ENUMTYPE meshtastic_ModuleConfig_SerialConfig_Serial_Baud +#define meshtastic_ModuleConfig_SerialConfig_mode_ENUMTYPE meshtastic_ModuleConfig_SerialConfig_Serial_Mode + + + + + +#define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_event_cw_ENUMTYPE meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar +#define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_event_ccw_ENUMTYPE meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar +#define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_event_press_ENUMTYPE meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar + + +#define meshtastic_RemoteHardwarePin_type_ENUMTYPE meshtastic_RemoteHardwarePinType + + +/* Initializer values for message structs */ +#define meshtastic_ModuleConfig_init_default {0, {meshtastic_ModuleConfig_MQTTConfig_init_default}} +#define meshtastic_ModuleConfig_MQTTConfig_init_default {0, "", "", "", 0, 0, 0, "", 0, 0, false, meshtastic_ModuleConfig_MapReportSettings_init_default} +#define meshtastic_ModuleConfig_MapReportSettings_init_default {0, 0} +#define meshtastic_ModuleConfig_RemoteHardwareConfig_init_default {0, 0, 0, {meshtastic_RemoteHardwarePin_init_default, meshtastic_RemoteHardwarePin_init_default, meshtastic_RemoteHardwarePin_init_default, meshtastic_RemoteHardwarePin_init_default}} +#define meshtastic_ModuleConfig_NeighborInfoConfig_init_default {0, 0} +#define meshtastic_ModuleConfig_DetectionSensorConfig_init_default {0, 0, 0, 0, "", 0, _meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_MIN, 0} +#define meshtastic_ModuleConfig_AudioConfig_init_default {0, 0, _meshtastic_ModuleConfig_AudioConfig_Audio_Baud_MIN, 0, 0, 0, 0} +#define meshtastic_ModuleConfig_PaxcounterConfig_init_default {0, 0, 0, 0} +#define meshtastic_ModuleConfig_SerialConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Baud_MIN, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MIN, 0} +#define meshtastic_ModuleConfig_ExternalNotificationConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +#define meshtastic_ModuleConfig_StoreForwardConfig_init_default {0, 0, 0, 0, 0, 0} +#define meshtastic_ModuleConfig_RangeTestConfig_init_default {0, 0, 0} +#define meshtastic_ModuleConfig_TelemetryConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +#define meshtastic_ModuleConfig_CannedMessageConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0} +#define meshtastic_ModuleConfig_AmbientLightingConfig_init_default {0, 0, 0, 0, 0} +#define meshtastic_RemoteHardwarePin_init_default {0, "", _meshtastic_RemoteHardwarePinType_MIN} +#define meshtastic_ModuleConfig_init_zero {0, {meshtastic_ModuleConfig_MQTTConfig_init_zero}} +#define meshtastic_ModuleConfig_MQTTConfig_init_zero {0, "", "", "", 0, 0, 0, "", 0, 0, false, meshtastic_ModuleConfig_MapReportSettings_init_zero} +#define meshtastic_ModuleConfig_MapReportSettings_init_zero {0, 0} +#define meshtastic_ModuleConfig_RemoteHardwareConfig_init_zero {0, 0, 0, {meshtastic_RemoteHardwarePin_init_zero, meshtastic_RemoteHardwarePin_init_zero, meshtastic_RemoteHardwarePin_init_zero, meshtastic_RemoteHardwarePin_init_zero}} +#define meshtastic_ModuleConfig_NeighborInfoConfig_init_zero {0, 0} +#define meshtastic_ModuleConfig_DetectionSensorConfig_init_zero {0, 0, 0, 0, "", 0, _meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_MIN, 0} +#define meshtastic_ModuleConfig_AudioConfig_init_zero {0, 0, _meshtastic_ModuleConfig_AudioConfig_Audio_Baud_MIN, 0, 0, 0, 0} +#define meshtastic_ModuleConfig_PaxcounterConfig_init_zero {0, 0, 0, 0} +#define meshtastic_ModuleConfig_SerialConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Baud_MIN, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MIN, 0} +#define meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +#define meshtastic_ModuleConfig_StoreForwardConfig_init_zero {0, 0, 0, 0, 0, 0} +#define meshtastic_ModuleConfig_RangeTestConfig_init_zero {0, 0, 0} +#define meshtastic_ModuleConfig_TelemetryConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +#define meshtastic_ModuleConfig_CannedMessageConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0} +#define meshtastic_ModuleConfig_AmbientLightingConfig_init_zero {0, 0, 0, 0, 0} +#define meshtastic_RemoteHardwarePin_init_zero {0, "", _meshtastic_RemoteHardwarePinType_MIN} + +/* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_ModuleConfig_MapReportSettings_publish_interval_secs_tag 1 +#define meshtastic_ModuleConfig_MapReportSettings_position_precision_tag 2 +#define meshtastic_ModuleConfig_MQTTConfig_enabled_tag 1 +#define meshtastic_ModuleConfig_MQTTConfig_address_tag 2 +#define meshtastic_ModuleConfig_MQTTConfig_username_tag 3 +#define meshtastic_ModuleConfig_MQTTConfig_password_tag 4 +#define meshtastic_ModuleConfig_MQTTConfig_encryption_enabled_tag 5 +#define meshtastic_ModuleConfig_MQTTConfig_json_enabled_tag 6 +#define meshtastic_ModuleConfig_MQTTConfig_tls_enabled_tag 7 +#define meshtastic_ModuleConfig_MQTTConfig_root_tag 8 +#define meshtastic_ModuleConfig_MQTTConfig_proxy_to_client_enabled_tag 9 +#define meshtastic_ModuleConfig_MQTTConfig_map_reporting_enabled_tag 10 +#define meshtastic_ModuleConfig_MQTTConfig_map_report_settings_tag 11 +#define meshtastic_ModuleConfig_NeighborInfoConfig_enabled_tag 1 +#define meshtastic_ModuleConfig_NeighborInfoConfig_update_interval_tag 2 +#define meshtastic_ModuleConfig_DetectionSensorConfig_enabled_tag 1 +#define meshtastic_ModuleConfig_DetectionSensorConfig_minimum_broadcast_secs_tag 2 +#define meshtastic_ModuleConfig_DetectionSensorConfig_state_broadcast_secs_tag 3 +#define meshtastic_ModuleConfig_DetectionSensorConfig_send_bell_tag 4 +#define meshtastic_ModuleConfig_DetectionSensorConfig_name_tag 5 +#define meshtastic_ModuleConfig_DetectionSensorConfig_monitor_pin_tag 6 +#define meshtastic_ModuleConfig_DetectionSensorConfig_detection_trigger_type_tag 7 +#define meshtastic_ModuleConfig_DetectionSensorConfig_use_pullup_tag 8 +#define meshtastic_ModuleConfig_AudioConfig_codec2_enabled_tag 1 +#define meshtastic_ModuleConfig_AudioConfig_ptt_pin_tag 2 +#define meshtastic_ModuleConfig_AudioConfig_bitrate_tag 3 +#define meshtastic_ModuleConfig_AudioConfig_i2s_ws_tag 4 +#define meshtastic_ModuleConfig_AudioConfig_i2s_sd_tag 5 +#define meshtastic_ModuleConfig_AudioConfig_i2s_din_tag 6 +#define meshtastic_ModuleConfig_AudioConfig_i2s_sck_tag 7 +#define meshtastic_ModuleConfig_PaxcounterConfig_enabled_tag 1 +#define meshtastic_ModuleConfig_PaxcounterConfig_paxcounter_update_interval_tag 2 +#define meshtastic_ModuleConfig_PaxcounterConfig_wifi_threshold_tag 3 +#define meshtastic_ModuleConfig_PaxcounterConfig_ble_threshold_tag 4 +#define meshtastic_ModuleConfig_SerialConfig_enabled_tag 1 +#define meshtastic_ModuleConfig_SerialConfig_echo_tag 2 +#define meshtastic_ModuleConfig_SerialConfig_rxd_tag 3 +#define meshtastic_ModuleConfig_SerialConfig_txd_tag 4 +#define meshtastic_ModuleConfig_SerialConfig_baud_tag 5 +#define meshtastic_ModuleConfig_SerialConfig_timeout_tag 6 +#define meshtastic_ModuleConfig_SerialConfig_mode_tag 7 +#define meshtastic_ModuleConfig_SerialConfig_override_console_serial_port_tag 8 +#define meshtastic_ModuleConfig_ExternalNotificationConfig_enabled_tag 1 +#define meshtastic_ModuleConfig_ExternalNotificationConfig_output_ms_tag 2 +#define meshtastic_ModuleConfig_ExternalNotificationConfig_output_tag 3 +#define meshtastic_ModuleConfig_ExternalNotificationConfig_active_tag 4 +#define meshtastic_ModuleConfig_ExternalNotificationConfig_alert_message_tag 5 +#define meshtastic_ModuleConfig_ExternalNotificationConfig_alert_bell_tag 6 +#define meshtastic_ModuleConfig_ExternalNotificationConfig_use_pwm_tag 7 +#define meshtastic_ModuleConfig_ExternalNotificationConfig_output_vibra_tag 8 +#define meshtastic_ModuleConfig_ExternalNotificationConfig_output_buzzer_tag 9 +#define meshtastic_ModuleConfig_ExternalNotificationConfig_alert_message_vibra_tag 10 +#define meshtastic_ModuleConfig_ExternalNotificationConfig_alert_message_buzzer_tag 11 +#define meshtastic_ModuleConfig_ExternalNotificationConfig_alert_bell_vibra_tag 12 +#define meshtastic_ModuleConfig_ExternalNotificationConfig_alert_bell_buzzer_tag 13 +#define meshtastic_ModuleConfig_ExternalNotificationConfig_nag_timeout_tag 14 +#define meshtastic_ModuleConfig_ExternalNotificationConfig_use_i2s_as_buzzer_tag 15 +#define meshtastic_ModuleConfig_StoreForwardConfig_enabled_tag 1 +#define meshtastic_ModuleConfig_StoreForwardConfig_heartbeat_tag 2 +#define meshtastic_ModuleConfig_StoreForwardConfig_records_tag 3 +#define meshtastic_ModuleConfig_StoreForwardConfig_history_return_max_tag 4 +#define meshtastic_ModuleConfig_StoreForwardConfig_history_return_window_tag 5 +#define meshtastic_ModuleConfig_StoreForwardConfig_is_server_tag 6 +#define meshtastic_ModuleConfig_RangeTestConfig_enabled_tag 1 +#define meshtastic_ModuleConfig_RangeTestConfig_sender_tag 2 +#define meshtastic_ModuleConfig_RangeTestConfig_save_tag 3 +#define meshtastic_ModuleConfig_TelemetryConfig_device_update_interval_tag 1 +#define meshtastic_ModuleConfig_TelemetryConfig_environment_update_interval_tag 2 +#define meshtastic_ModuleConfig_TelemetryConfig_environment_measurement_enabled_tag 3 +#define meshtastic_ModuleConfig_TelemetryConfig_environment_screen_enabled_tag 4 +#define meshtastic_ModuleConfig_TelemetryConfig_environment_display_fahrenheit_tag 5 +#define meshtastic_ModuleConfig_TelemetryConfig_air_quality_enabled_tag 6 +#define meshtastic_ModuleConfig_TelemetryConfig_air_quality_interval_tag 7 +#define meshtastic_ModuleConfig_TelemetryConfig_power_measurement_enabled_tag 8 +#define meshtastic_ModuleConfig_TelemetryConfig_power_update_interval_tag 9 +#define meshtastic_ModuleConfig_TelemetryConfig_power_screen_enabled_tag 10 +#define meshtastic_ModuleConfig_TelemetryConfig_health_measurement_enabled_tag 11 +#define meshtastic_ModuleConfig_TelemetryConfig_health_update_interval_tag 12 +#define meshtastic_ModuleConfig_TelemetryConfig_health_screen_enabled_tag 13 +#define meshtastic_ModuleConfig_CannedMessageConfig_rotary1_enabled_tag 1 +#define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_pin_a_tag 2 +#define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_pin_b_tag 3 +#define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_pin_press_tag 4 +#define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_event_cw_tag 5 +#define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_event_ccw_tag 6 +#define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_event_press_tag 7 +#define meshtastic_ModuleConfig_CannedMessageConfig_updown1_enabled_tag 8 +#define meshtastic_ModuleConfig_CannedMessageConfig_enabled_tag 9 +#define meshtastic_ModuleConfig_CannedMessageConfig_allow_input_source_tag 10 +#define meshtastic_ModuleConfig_CannedMessageConfig_send_bell_tag 11 +#define meshtastic_ModuleConfig_AmbientLightingConfig_led_state_tag 1 +#define meshtastic_ModuleConfig_AmbientLightingConfig_current_tag 2 +#define meshtastic_ModuleConfig_AmbientLightingConfig_red_tag 3 +#define meshtastic_ModuleConfig_AmbientLightingConfig_green_tag 4 +#define meshtastic_ModuleConfig_AmbientLightingConfig_blue_tag 5 +#define meshtastic_RemoteHardwarePin_gpio_pin_tag 1 +#define meshtastic_RemoteHardwarePin_name_tag 2 +#define meshtastic_RemoteHardwarePin_type_tag 3 +#define meshtastic_ModuleConfig_RemoteHardwareConfig_enabled_tag 1 +#define meshtastic_ModuleConfig_RemoteHardwareConfig_allow_undefined_pin_access_tag 2 +#define meshtastic_ModuleConfig_RemoteHardwareConfig_available_pins_tag 3 +#define meshtastic_ModuleConfig_mqtt_tag 1 +#define meshtastic_ModuleConfig_serial_tag 2 +#define meshtastic_ModuleConfig_external_notification_tag 3 +#define meshtastic_ModuleConfig_store_forward_tag 4 +#define meshtastic_ModuleConfig_range_test_tag 5 +#define meshtastic_ModuleConfig_telemetry_tag 6 +#define meshtastic_ModuleConfig_canned_message_tag 7 +#define meshtastic_ModuleConfig_audio_tag 8 +#define meshtastic_ModuleConfig_remote_hardware_tag 9 +#define meshtastic_ModuleConfig_neighbor_info_tag 10 +#define meshtastic_ModuleConfig_ambient_lighting_tag 11 +#define meshtastic_ModuleConfig_detection_sensor_tag 12 +#define meshtastic_ModuleConfig_paxcounter_tag 13 + +/* Struct field encoding specification for nanopb */ +#define meshtastic_ModuleConfig_FIELDLIST(X, a) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,mqtt,payload_variant.mqtt), 1) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,serial,payload_variant.serial), 2) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,external_notification,payload_variant.external_notification), 3) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,store_forward,payload_variant.store_forward), 4) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,range_test,payload_variant.range_test), 5) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,telemetry,payload_variant.telemetry), 6) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,canned_message,payload_variant.canned_message), 7) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,audio,payload_variant.audio), 8) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,remote_hardware,payload_variant.remote_hardware), 9) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,neighbor_info,payload_variant.neighbor_info), 10) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,ambient_lighting,payload_variant.ambient_lighting), 11) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,detection_sensor,payload_variant.detection_sensor), 12) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,paxcounter,payload_variant.paxcounter), 13) +#define meshtastic_ModuleConfig_CALLBACK NULL +#define meshtastic_ModuleConfig_DEFAULT NULL +#define meshtastic_ModuleConfig_payload_variant_mqtt_MSGTYPE meshtastic_ModuleConfig_MQTTConfig +#define meshtastic_ModuleConfig_payload_variant_serial_MSGTYPE meshtastic_ModuleConfig_SerialConfig +#define meshtastic_ModuleConfig_payload_variant_external_notification_MSGTYPE meshtastic_ModuleConfig_ExternalNotificationConfig +#define meshtastic_ModuleConfig_payload_variant_store_forward_MSGTYPE meshtastic_ModuleConfig_StoreForwardConfig +#define meshtastic_ModuleConfig_payload_variant_range_test_MSGTYPE meshtastic_ModuleConfig_RangeTestConfig +#define meshtastic_ModuleConfig_payload_variant_telemetry_MSGTYPE meshtastic_ModuleConfig_TelemetryConfig +#define meshtastic_ModuleConfig_payload_variant_canned_message_MSGTYPE meshtastic_ModuleConfig_CannedMessageConfig +#define meshtastic_ModuleConfig_payload_variant_audio_MSGTYPE meshtastic_ModuleConfig_AudioConfig +#define meshtastic_ModuleConfig_payload_variant_remote_hardware_MSGTYPE meshtastic_ModuleConfig_RemoteHardwareConfig +#define meshtastic_ModuleConfig_payload_variant_neighbor_info_MSGTYPE meshtastic_ModuleConfig_NeighborInfoConfig +#define meshtastic_ModuleConfig_payload_variant_ambient_lighting_MSGTYPE meshtastic_ModuleConfig_AmbientLightingConfig +#define meshtastic_ModuleConfig_payload_variant_detection_sensor_MSGTYPE meshtastic_ModuleConfig_DetectionSensorConfig +#define meshtastic_ModuleConfig_payload_variant_paxcounter_MSGTYPE meshtastic_ModuleConfig_PaxcounterConfig + +#define meshtastic_ModuleConfig_MQTTConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, enabled, 1) \ +X(a, STATIC, SINGULAR, STRING, address, 2) \ +X(a, STATIC, SINGULAR, STRING, username, 3) \ +X(a, STATIC, SINGULAR, STRING, password, 4) \ +X(a, STATIC, SINGULAR, BOOL, encryption_enabled, 5) \ +X(a, STATIC, SINGULAR, BOOL, json_enabled, 6) \ +X(a, STATIC, SINGULAR, BOOL, tls_enabled, 7) \ +X(a, STATIC, SINGULAR, STRING, root, 8) \ +X(a, STATIC, SINGULAR, BOOL, proxy_to_client_enabled, 9) \ +X(a, STATIC, SINGULAR, BOOL, map_reporting_enabled, 10) \ +X(a, STATIC, OPTIONAL, MESSAGE, map_report_settings, 11) +#define meshtastic_ModuleConfig_MQTTConfig_CALLBACK NULL +#define meshtastic_ModuleConfig_MQTTConfig_DEFAULT NULL +#define meshtastic_ModuleConfig_MQTTConfig_map_report_settings_MSGTYPE meshtastic_ModuleConfig_MapReportSettings + +#define meshtastic_ModuleConfig_MapReportSettings_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, publish_interval_secs, 1) \ +X(a, STATIC, SINGULAR, UINT32, position_precision, 2) +#define meshtastic_ModuleConfig_MapReportSettings_CALLBACK NULL +#define meshtastic_ModuleConfig_MapReportSettings_DEFAULT NULL + +#define meshtastic_ModuleConfig_RemoteHardwareConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, enabled, 1) \ +X(a, STATIC, SINGULAR, BOOL, allow_undefined_pin_access, 2) \ +X(a, STATIC, REPEATED, MESSAGE, available_pins, 3) +#define meshtastic_ModuleConfig_RemoteHardwareConfig_CALLBACK NULL +#define meshtastic_ModuleConfig_RemoteHardwareConfig_DEFAULT NULL +#define meshtastic_ModuleConfig_RemoteHardwareConfig_available_pins_MSGTYPE meshtastic_RemoteHardwarePin + +#define meshtastic_ModuleConfig_NeighborInfoConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, enabled, 1) \ +X(a, STATIC, SINGULAR, UINT32, update_interval, 2) +#define meshtastic_ModuleConfig_NeighborInfoConfig_CALLBACK NULL +#define meshtastic_ModuleConfig_NeighborInfoConfig_DEFAULT NULL + +#define meshtastic_ModuleConfig_DetectionSensorConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, enabled, 1) \ +X(a, STATIC, SINGULAR, UINT32, minimum_broadcast_secs, 2) \ +X(a, STATIC, SINGULAR, UINT32, state_broadcast_secs, 3) \ +X(a, STATIC, SINGULAR, BOOL, send_bell, 4) \ +X(a, STATIC, SINGULAR, STRING, name, 5) \ +X(a, STATIC, SINGULAR, UINT32, monitor_pin, 6) \ +X(a, STATIC, SINGULAR, UENUM, detection_trigger_type, 7) \ +X(a, STATIC, SINGULAR, BOOL, use_pullup, 8) +#define meshtastic_ModuleConfig_DetectionSensorConfig_CALLBACK NULL +#define meshtastic_ModuleConfig_DetectionSensorConfig_DEFAULT NULL + +#define meshtastic_ModuleConfig_AudioConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, codec2_enabled, 1) \ +X(a, STATIC, SINGULAR, UINT32, ptt_pin, 2) \ +X(a, STATIC, SINGULAR, UENUM, bitrate, 3) \ +X(a, STATIC, SINGULAR, UINT32, i2s_ws, 4) \ +X(a, STATIC, SINGULAR, UINT32, i2s_sd, 5) \ +X(a, STATIC, SINGULAR, UINT32, i2s_din, 6) \ +X(a, STATIC, SINGULAR, UINT32, i2s_sck, 7) +#define meshtastic_ModuleConfig_AudioConfig_CALLBACK NULL +#define meshtastic_ModuleConfig_AudioConfig_DEFAULT NULL + +#define meshtastic_ModuleConfig_PaxcounterConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, enabled, 1) \ +X(a, STATIC, SINGULAR, UINT32, paxcounter_update_interval, 2) \ +X(a, STATIC, SINGULAR, INT32, wifi_threshold, 3) \ +X(a, STATIC, SINGULAR, INT32, ble_threshold, 4) +#define meshtastic_ModuleConfig_PaxcounterConfig_CALLBACK NULL +#define meshtastic_ModuleConfig_PaxcounterConfig_DEFAULT NULL + +#define meshtastic_ModuleConfig_SerialConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, enabled, 1) \ +X(a, STATIC, SINGULAR, BOOL, echo, 2) \ +X(a, STATIC, SINGULAR, UINT32, rxd, 3) \ +X(a, STATIC, SINGULAR, UINT32, txd, 4) \ +X(a, STATIC, SINGULAR, UENUM, baud, 5) \ +X(a, STATIC, SINGULAR, UINT32, timeout, 6) \ +X(a, STATIC, SINGULAR, UENUM, mode, 7) \ +X(a, STATIC, SINGULAR, BOOL, override_console_serial_port, 8) +#define meshtastic_ModuleConfig_SerialConfig_CALLBACK NULL +#define meshtastic_ModuleConfig_SerialConfig_DEFAULT NULL + +#define meshtastic_ModuleConfig_ExternalNotificationConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, enabled, 1) \ +X(a, STATIC, SINGULAR, UINT32, output_ms, 2) \ +X(a, STATIC, SINGULAR, UINT32, output, 3) \ +X(a, STATIC, SINGULAR, BOOL, active, 4) \ +X(a, STATIC, SINGULAR, BOOL, alert_message, 5) \ +X(a, STATIC, SINGULAR, BOOL, alert_bell, 6) \ +X(a, STATIC, SINGULAR, BOOL, use_pwm, 7) \ +X(a, STATIC, SINGULAR, UINT32, output_vibra, 8) \ +X(a, STATIC, SINGULAR, UINT32, output_buzzer, 9) \ +X(a, STATIC, SINGULAR, BOOL, alert_message_vibra, 10) \ +X(a, STATIC, SINGULAR, BOOL, alert_message_buzzer, 11) \ +X(a, STATIC, SINGULAR, BOOL, alert_bell_vibra, 12) \ +X(a, STATIC, SINGULAR, BOOL, alert_bell_buzzer, 13) \ +X(a, STATIC, SINGULAR, UINT32, nag_timeout, 14) \ +X(a, STATIC, SINGULAR, BOOL, use_i2s_as_buzzer, 15) +#define meshtastic_ModuleConfig_ExternalNotificationConfig_CALLBACK NULL +#define meshtastic_ModuleConfig_ExternalNotificationConfig_DEFAULT NULL + +#define meshtastic_ModuleConfig_StoreForwardConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, enabled, 1) \ +X(a, STATIC, SINGULAR, BOOL, heartbeat, 2) \ +X(a, STATIC, SINGULAR, UINT32, records, 3) \ +X(a, STATIC, SINGULAR, UINT32, history_return_max, 4) \ +X(a, STATIC, SINGULAR, UINT32, history_return_window, 5) \ +X(a, STATIC, SINGULAR, BOOL, is_server, 6) +#define meshtastic_ModuleConfig_StoreForwardConfig_CALLBACK NULL +#define meshtastic_ModuleConfig_StoreForwardConfig_DEFAULT NULL + +#define meshtastic_ModuleConfig_RangeTestConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, enabled, 1) \ +X(a, STATIC, SINGULAR, UINT32, sender, 2) \ +X(a, STATIC, SINGULAR, BOOL, save, 3) +#define meshtastic_ModuleConfig_RangeTestConfig_CALLBACK NULL +#define meshtastic_ModuleConfig_RangeTestConfig_DEFAULT NULL + +#define meshtastic_ModuleConfig_TelemetryConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, device_update_interval, 1) \ +X(a, STATIC, SINGULAR, UINT32, environment_update_interval, 2) \ +X(a, STATIC, SINGULAR, BOOL, environment_measurement_enabled, 3) \ +X(a, STATIC, SINGULAR, BOOL, environment_screen_enabled, 4) \ +X(a, STATIC, SINGULAR, BOOL, environment_display_fahrenheit, 5) \ +X(a, STATIC, SINGULAR, BOOL, air_quality_enabled, 6) \ +X(a, STATIC, SINGULAR, UINT32, air_quality_interval, 7) \ +X(a, STATIC, SINGULAR, BOOL, power_measurement_enabled, 8) \ +X(a, STATIC, SINGULAR, UINT32, power_update_interval, 9) \ +X(a, STATIC, SINGULAR, BOOL, power_screen_enabled, 10) \ +X(a, STATIC, SINGULAR, BOOL, health_measurement_enabled, 11) \ +X(a, STATIC, SINGULAR, UINT32, health_update_interval, 12) \ +X(a, STATIC, SINGULAR, BOOL, health_screen_enabled, 13) +#define meshtastic_ModuleConfig_TelemetryConfig_CALLBACK NULL +#define meshtastic_ModuleConfig_TelemetryConfig_DEFAULT NULL + +#define meshtastic_ModuleConfig_CannedMessageConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, rotary1_enabled, 1) \ +X(a, STATIC, SINGULAR, UINT32, inputbroker_pin_a, 2) \ +X(a, STATIC, SINGULAR, UINT32, inputbroker_pin_b, 3) \ +X(a, STATIC, SINGULAR, UINT32, inputbroker_pin_press, 4) \ +X(a, STATIC, SINGULAR, UENUM, inputbroker_event_cw, 5) \ +X(a, STATIC, SINGULAR, UENUM, inputbroker_event_ccw, 6) \ +X(a, STATIC, SINGULAR, UENUM, inputbroker_event_press, 7) \ +X(a, STATIC, SINGULAR, BOOL, updown1_enabled, 8) \ +X(a, STATIC, SINGULAR, BOOL, enabled, 9) \ +X(a, STATIC, SINGULAR, STRING, allow_input_source, 10) \ +X(a, STATIC, SINGULAR, BOOL, send_bell, 11) +#define meshtastic_ModuleConfig_CannedMessageConfig_CALLBACK NULL +#define meshtastic_ModuleConfig_CannedMessageConfig_DEFAULT NULL + +#define meshtastic_ModuleConfig_AmbientLightingConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, led_state, 1) \ +X(a, STATIC, SINGULAR, UINT32, current, 2) \ +X(a, STATIC, SINGULAR, UINT32, red, 3) \ +X(a, STATIC, SINGULAR, UINT32, green, 4) \ +X(a, STATIC, SINGULAR, UINT32, blue, 5) +#define meshtastic_ModuleConfig_AmbientLightingConfig_CALLBACK NULL +#define meshtastic_ModuleConfig_AmbientLightingConfig_DEFAULT NULL + +#define meshtastic_RemoteHardwarePin_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, gpio_pin, 1) \ +X(a, STATIC, SINGULAR, STRING, name, 2) \ +X(a, STATIC, SINGULAR, UENUM, type, 3) +#define meshtastic_RemoteHardwarePin_CALLBACK NULL +#define meshtastic_RemoteHardwarePin_DEFAULT NULL + +extern const pb_msgdesc_t meshtastic_ModuleConfig_msg; +extern const pb_msgdesc_t meshtastic_ModuleConfig_MQTTConfig_msg; +extern const pb_msgdesc_t meshtastic_ModuleConfig_MapReportSettings_msg; +extern const pb_msgdesc_t meshtastic_ModuleConfig_RemoteHardwareConfig_msg; +extern const pb_msgdesc_t meshtastic_ModuleConfig_NeighborInfoConfig_msg; +extern const pb_msgdesc_t meshtastic_ModuleConfig_DetectionSensorConfig_msg; +extern const pb_msgdesc_t meshtastic_ModuleConfig_AudioConfig_msg; +extern const pb_msgdesc_t meshtastic_ModuleConfig_PaxcounterConfig_msg; +extern const pb_msgdesc_t meshtastic_ModuleConfig_SerialConfig_msg; +extern const pb_msgdesc_t meshtastic_ModuleConfig_ExternalNotificationConfig_msg; +extern const pb_msgdesc_t meshtastic_ModuleConfig_StoreForwardConfig_msg; +extern const pb_msgdesc_t meshtastic_ModuleConfig_RangeTestConfig_msg; +extern const pb_msgdesc_t meshtastic_ModuleConfig_TelemetryConfig_msg; +extern const pb_msgdesc_t meshtastic_ModuleConfig_CannedMessageConfig_msg; +extern const pb_msgdesc_t meshtastic_ModuleConfig_AmbientLightingConfig_msg; +extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define meshtastic_ModuleConfig_fields &meshtastic_ModuleConfig_msg +#define meshtastic_ModuleConfig_MQTTConfig_fields &meshtastic_ModuleConfig_MQTTConfig_msg +#define meshtastic_ModuleConfig_MapReportSettings_fields &meshtastic_ModuleConfig_MapReportSettings_msg +#define meshtastic_ModuleConfig_RemoteHardwareConfig_fields &meshtastic_ModuleConfig_RemoteHardwareConfig_msg +#define meshtastic_ModuleConfig_NeighborInfoConfig_fields &meshtastic_ModuleConfig_NeighborInfoConfig_msg +#define meshtastic_ModuleConfig_DetectionSensorConfig_fields &meshtastic_ModuleConfig_DetectionSensorConfig_msg +#define meshtastic_ModuleConfig_AudioConfig_fields &meshtastic_ModuleConfig_AudioConfig_msg +#define meshtastic_ModuleConfig_PaxcounterConfig_fields &meshtastic_ModuleConfig_PaxcounterConfig_msg +#define meshtastic_ModuleConfig_SerialConfig_fields &meshtastic_ModuleConfig_SerialConfig_msg +#define meshtastic_ModuleConfig_ExternalNotificationConfig_fields &meshtastic_ModuleConfig_ExternalNotificationConfig_msg +#define meshtastic_ModuleConfig_StoreForwardConfig_fields &meshtastic_ModuleConfig_StoreForwardConfig_msg +#define meshtastic_ModuleConfig_RangeTestConfig_fields &meshtastic_ModuleConfig_RangeTestConfig_msg +#define meshtastic_ModuleConfig_TelemetryConfig_fields &meshtastic_ModuleConfig_TelemetryConfig_msg +#define meshtastic_ModuleConfig_CannedMessageConfig_fields &meshtastic_ModuleConfig_CannedMessageConfig_msg +#define meshtastic_ModuleConfig_AmbientLightingConfig_fields &meshtastic_ModuleConfig_AmbientLightingConfig_msg +#define meshtastic_RemoteHardwarePin_fields &meshtastic_RemoteHardwarePin_msg + +/* Maximum encoded size of messages (where known) */ +#define MESHTASTIC_MESHTASTIC_MODULE_CONFIG_PB_H_MAX_SIZE meshtastic_ModuleConfig_size +#define meshtastic_ModuleConfig_AmbientLightingConfig_size 14 +#define meshtastic_ModuleConfig_AudioConfig_size 19 +#define meshtastic_ModuleConfig_CannedMessageConfig_size 49 +#define meshtastic_ModuleConfig_DetectionSensorConfig_size 44 +#define meshtastic_ModuleConfig_ExternalNotificationConfig_size 42 +#define meshtastic_ModuleConfig_MQTTConfig_size 254 +#define meshtastic_ModuleConfig_MapReportSettings_size 12 +#define meshtastic_ModuleConfig_NeighborInfoConfig_size 8 +#define meshtastic_ModuleConfig_PaxcounterConfig_size 30 +#define meshtastic_ModuleConfig_RangeTestConfig_size 10 +#define meshtastic_ModuleConfig_RemoteHardwareConfig_size 96 +#define meshtastic_ModuleConfig_SerialConfig_size 28 +#define meshtastic_ModuleConfig_StoreForwardConfig_size 24 +#define meshtastic_ModuleConfig_TelemetryConfig_size 46 +#define meshtastic_ModuleConfig_size 257 +#define meshtastic_RemoteHardwarePin_size 21 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/mesh/generated/meshtastic/mqtt.pb.cpp b/src/mesh/generated/meshtastic/mqtt.pb.cpp new file mode 100644 index 0000000..74536cb --- /dev/null +++ b/src/mesh/generated/meshtastic/mqtt.pb.cpp @@ -0,0 +1,15 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9 */ + +#include "meshtastic/mqtt.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(meshtastic_ServiceEnvelope, meshtastic_ServiceEnvelope, AUTO) + + +PB_BIND(meshtastic_MapReport, meshtastic_MapReport, AUTO) + + + diff --git a/src/mesh/generated/meshtastic/mqtt.pb.h b/src/mesh/generated/meshtastic/mqtt.pb.h new file mode 100644 index 0000000..4d10273 --- /dev/null +++ b/src/mesh/generated/meshtastic/mqtt.pb.h @@ -0,0 +1,130 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9 */ + +#ifndef PB_MESHTASTIC_MESHTASTIC_MQTT_PB_H_INCLUDED +#define PB_MESHTASTIC_MESHTASTIC_MQTT_PB_H_INCLUDED +#include +#include "meshtastic/config.pb.h" +#include "meshtastic/mesh.pb.h" + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Struct definitions */ +/* This message wraps a MeshPacket with extra metadata about the sender and how it arrived. */ +typedef struct _meshtastic_ServiceEnvelope { + /* The (probably encrypted) packet */ + struct _meshtastic_MeshPacket *packet; + /* The global channel ID it was sent on */ + char *channel_id; + /* The sending gateway node ID. Can we use this to authenticate/prevent fake + nodeid impersonation for senders? - i.e. use gateway/mesh id (which is authenticated) + local node id as + the globally trusted nodenum */ + char *gateway_id; +} meshtastic_ServiceEnvelope; + +/* Information about a node intended to be reported unencrypted to a map using MQTT. */ +typedef struct _meshtastic_MapReport { + /* A full name for this user, i.e. "Kevin Hester" */ + char long_name[40]; + /* A VERY short name, ideally two characters. + Suitable for a tiny OLED screen */ + char short_name[5]; + /* Role of the node that applies specific settings for a particular use-case */ + meshtastic_Config_DeviceConfig_Role role; + /* Hardware model of the node, i.e. T-Beam, Heltec V3, etc... */ + meshtastic_HardwareModel hw_model; + /* Device firmware version string */ + char firmware_version[18]; + /* The region code for the radio (US, CN, EU433, etc...) */ + meshtastic_Config_LoRaConfig_RegionCode region; + /* Modem preset used by the radio (LongFast, MediumSlow, etc...) */ + meshtastic_Config_LoRaConfig_ModemPreset modem_preset; + /* Whether the node has a channel with default PSK and name (LongFast, MediumSlow, etc...) + and it uses the default frequency slot given the region and modem preset. */ + bool has_default_channel; + /* Latitude: multiply by 1e-7 to get degrees in floating point */ + int32_t latitude_i; + /* Longitude: multiply by 1e-7 to get degrees in floating point */ + int32_t longitude_i; + /* Altitude in meters above MSL */ + int32_t altitude; + /* Indicates the bits of precision for latitude and longitude set by the sending node */ + uint32_t position_precision; + /* Number of online nodes (heard in the last 2 hours) this node has in its list that were received locally (not via MQTT) */ + uint16_t num_online_local_nodes; +} meshtastic_MapReport; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initializer values for message structs */ +#define meshtastic_ServiceEnvelope_init_default {NULL, NULL, NULL} +#define meshtastic_MapReport_init_default {"", "", _meshtastic_Config_DeviceConfig_Role_MIN, _meshtastic_HardwareModel_MIN, "", _meshtastic_Config_LoRaConfig_RegionCode_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, 0, 0} +#define meshtastic_ServiceEnvelope_init_zero {NULL, NULL, NULL} +#define meshtastic_MapReport_init_zero {"", "", _meshtastic_Config_DeviceConfig_Role_MIN, _meshtastic_HardwareModel_MIN, "", _meshtastic_Config_LoRaConfig_RegionCode_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, 0, 0} + +/* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_ServiceEnvelope_packet_tag 1 +#define meshtastic_ServiceEnvelope_channel_id_tag 2 +#define meshtastic_ServiceEnvelope_gateway_id_tag 3 +#define meshtastic_MapReport_long_name_tag 1 +#define meshtastic_MapReport_short_name_tag 2 +#define meshtastic_MapReport_role_tag 3 +#define meshtastic_MapReport_hw_model_tag 4 +#define meshtastic_MapReport_firmware_version_tag 5 +#define meshtastic_MapReport_region_tag 6 +#define meshtastic_MapReport_modem_preset_tag 7 +#define meshtastic_MapReport_has_default_channel_tag 8 +#define meshtastic_MapReport_latitude_i_tag 9 +#define meshtastic_MapReport_longitude_i_tag 10 +#define meshtastic_MapReport_altitude_tag 11 +#define meshtastic_MapReport_position_precision_tag 12 +#define meshtastic_MapReport_num_online_local_nodes_tag 13 + +/* Struct field encoding specification for nanopb */ +#define meshtastic_ServiceEnvelope_FIELDLIST(X, a) \ +X(a, POINTER, OPTIONAL, MESSAGE, packet, 1) \ +X(a, POINTER, SINGULAR, STRING, channel_id, 2) \ +X(a, POINTER, SINGULAR, STRING, gateway_id, 3) +#define meshtastic_ServiceEnvelope_CALLBACK NULL +#define meshtastic_ServiceEnvelope_DEFAULT NULL +#define meshtastic_ServiceEnvelope_packet_MSGTYPE meshtastic_MeshPacket + +#define meshtastic_MapReport_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, STRING, long_name, 1) \ +X(a, STATIC, SINGULAR, STRING, short_name, 2) \ +X(a, STATIC, SINGULAR, UENUM, role, 3) \ +X(a, STATIC, SINGULAR, UENUM, hw_model, 4) \ +X(a, STATIC, SINGULAR, STRING, firmware_version, 5) \ +X(a, STATIC, SINGULAR, UENUM, region, 6) \ +X(a, STATIC, SINGULAR, UENUM, modem_preset, 7) \ +X(a, STATIC, SINGULAR, BOOL, has_default_channel, 8) \ +X(a, STATIC, SINGULAR, SFIXED32, latitude_i, 9) \ +X(a, STATIC, SINGULAR, SFIXED32, longitude_i, 10) \ +X(a, STATIC, SINGULAR, INT32, altitude, 11) \ +X(a, STATIC, SINGULAR, UINT32, position_precision, 12) \ +X(a, STATIC, SINGULAR, UINT32, num_online_local_nodes, 13) +#define meshtastic_MapReport_CALLBACK NULL +#define meshtastic_MapReport_DEFAULT NULL + +extern const pb_msgdesc_t meshtastic_ServiceEnvelope_msg; +extern const pb_msgdesc_t meshtastic_MapReport_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define meshtastic_ServiceEnvelope_fields &meshtastic_ServiceEnvelope_msg +#define meshtastic_MapReport_fields &meshtastic_MapReport_msg + +/* Maximum encoded size of messages (where known) */ +/* meshtastic_ServiceEnvelope_size depends on runtime parameters */ +#define MESHTASTIC_MESHTASTIC_MQTT_PB_H_MAX_SIZE meshtastic_MapReport_size +#define meshtastic_MapReport_size 108 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/mesh/generated/meshtastic/paxcount.pb.cpp b/src/mesh/generated/meshtastic/paxcount.pb.cpp new file mode 100644 index 0000000..4032881 --- /dev/null +++ b/src/mesh/generated/meshtastic/paxcount.pb.cpp @@ -0,0 +1,12 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9 */ + +#include "meshtastic/paxcount.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(meshtastic_Paxcount, meshtastic_Paxcount, AUTO) + + + diff --git a/src/mesh/generated/meshtastic/paxcount.pb.h b/src/mesh/generated/meshtastic/paxcount.pb.h new file mode 100644 index 0000000..b6b51fd --- /dev/null +++ b/src/mesh/generated/meshtastic/paxcount.pb.h @@ -0,0 +1,58 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9 */ + +#ifndef PB_MESHTASTIC_MESHTASTIC_PAXCOUNT_PB_H_INCLUDED +#define PB_MESHTASTIC_MESHTASTIC_PAXCOUNT_PB_H_INCLUDED +#include + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Struct definitions */ +/* TODO: REPLACE */ +typedef struct _meshtastic_Paxcount { + /* seen Wifi devices */ + uint32_t wifi; + /* Seen BLE devices */ + uint32_t ble; + /* Uptime in seconds */ + uint32_t uptime; +} meshtastic_Paxcount; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initializer values for message structs */ +#define meshtastic_Paxcount_init_default {0, 0, 0} +#define meshtastic_Paxcount_init_zero {0, 0, 0} + +/* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_Paxcount_wifi_tag 1 +#define meshtastic_Paxcount_ble_tag 2 +#define meshtastic_Paxcount_uptime_tag 3 + +/* Struct field encoding specification for nanopb */ +#define meshtastic_Paxcount_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, wifi, 1) \ +X(a, STATIC, SINGULAR, UINT32, ble, 2) \ +X(a, STATIC, SINGULAR, UINT32, uptime, 3) +#define meshtastic_Paxcount_CALLBACK NULL +#define meshtastic_Paxcount_DEFAULT NULL + +extern const pb_msgdesc_t meshtastic_Paxcount_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define meshtastic_Paxcount_fields &meshtastic_Paxcount_msg + +/* Maximum encoded size of messages (where known) */ +#define MESHTASTIC_MESHTASTIC_PAXCOUNT_PB_H_MAX_SIZE meshtastic_Paxcount_size +#define meshtastic_Paxcount_size 18 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/mesh/generated/meshtastic/portnums.pb.cpp b/src/mesh/generated/meshtastic/portnums.pb.cpp new file mode 100644 index 0000000..8fca9af --- /dev/null +++ b/src/mesh/generated/meshtastic/portnums.pb.cpp @@ -0,0 +1,11 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9 */ + +#include "meshtastic/portnums.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + + + + diff --git a/src/mesh/generated/meshtastic/portnums.pb.h b/src/mesh/generated/meshtastic/portnums.pb.h new file mode 100644 index 0000000..df6cf32 --- /dev/null +++ b/src/mesh/generated/meshtastic/portnums.pb.h @@ -0,0 +1,154 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9 */ + +#ifndef PB_MESHTASTIC_MESHTASTIC_PORTNUMS_PB_H_INCLUDED +#define PB_MESHTASTIC_MESHTASTIC_PORTNUMS_PB_H_INCLUDED +#include + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Enum definitions */ +/* For any new 'apps' that run on the device or via sister apps on phones/PCs they should pick and use a + unique 'portnum' for their application. + If you are making a new app using meshtastic, please send in a pull request to add your 'portnum' to this + master table. + PortNums should be assigned in the following range: + 0-63 Core Meshtastic use, do not use for third party apps + 64-127 Registered 3rd party apps, send in a pull request that adds a new entry to portnums.proto to register your application + 256-511 Use one of these portnums for your private applications that you don't want to register publically + All other values are reserved. + Note: This was formerly a Type enum named 'typ' with the same id # + We have change to this 'portnum' based scheme for specifying app handlers for particular payloads. + This change is backwards compatible by treating the legacy OPAQUE/CLEAR_TEXT values identically. */ +typedef enum _meshtastic_PortNum { + /* Deprecated: do not use in new code (formerly called OPAQUE) + A message sent from a device outside of the mesh, in a form the mesh does not understand + NOTE: This must be 0, because it is documented in IMeshService.aidl to be so + ENCODING: binary undefined */ + meshtastic_PortNum_UNKNOWN_APP = 0, + /* A simple UTF-8 text message, which even the little micros in the mesh + can understand and show on their screen eventually in some circumstances + even signal might send messages in this form (see below) + ENCODING: UTF-8 Plaintext (?) */ + meshtastic_PortNum_TEXT_MESSAGE_APP = 1, + /* Reserved for built-in GPIO/example app. + See remote_hardware.proto/HardwareMessage for details on the message sent/received to this port number + ENCODING: Protobuf */ + meshtastic_PortNum_REMOTE_HARDWARE_APP = 2, + /* The built-in position messaging app. + Payload is a Position message. + ENCODING: Protobuf */ + meshtastic_PortNum_POSITION_APP = 3, + /* The built-in user info app. + Payload is a User message. + ENCODING: Protobuf */ + meshtastic_PortNum_NODEINFO_APP = 4, + /* Protocol control packets for mesh protocol use. + Payload is a Routing message. + ENCODING: Protobuf */ + meshtastic_PortNum_ROUTING_APP = 5, + /* Admin control packets. + Payload is a AdminMessage message. + ENCODING: Protobuf */ + meshtastic_PortNum_ADMIN_APP = 6, + /* Compressed TEXT_MESSAGE payloads. + ENCODING: UTF-8 Plaintext (?) with Unishox2 Compression + NOTE: The Device Firmware converts a TEXT_MESSAGE_APP to TEXT_MESSAGE_COMPRESSED_APP if the compressed + payload is shorter. There's no need for app developers to do this themselves. Also the firmware will decompress + any incoming TEXT_MESSAGE_COMPRESSED_APP payload and convert to TEXT_MESSAGE_APP. */ + meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP = 7, + /* Waypoint payloads. + Payload is a Waypoint message. + ENCODING: Protobuf */ + meshtastic_PortNum_WAYPOINT_APP = 8, + /* Audio Payloads. + Encapsulated codec2 packets. On 2.4 GHZ Bandwidths only for now + ENCODING: codec2 audio frames + NOTE: audio frames contain a 3 byte header (0xc0 0xde 0xc2) and a one byte marker for the decompressed bitrate. + This marker comes from the 'moduleConfig.audio.bitrate' enum minus one. */ + meshtastic_PortNum_AUDIO_APP = 9, + /* Same as Text Message but originating from Detection Sensor Module. + NOTE: This portnum traffic is not sent to the public MQTT starting at firmware version 2.2.9 */ + meshtastic_PortNum_DETECTION_SENSOR_APP = 10, + /* Provides a 'ping' service that replies to any packet it receives. + Also serves as a small example module. + ENCODING: ASCII Plaintext */ + meshtastic_PortNum_REPLY_APP = 32, + /* Used for the python IP tunnel feature + ENCODING: IP Packet. Handled by the python API, firmware ignores this one and pases on. */ + meshtastic_PortNum_IP_TUNNEL_APP = 33, + /* Paxcounter lib included in the firmware + ENCODING: protobuf */ + meshtastic_PortNum_PAXCOUNTER_APP = 34, + /* Provides a hardware serial interface to send and receive from the Meshtastic network. + Connect to the RX/TX pins of a device with 38400 8N1. Packets received from the Meshtastic + network is forwarded to the RX pin while sending a packet to TX will go out to the Mesh network. + Maximum packet size of 240 bytes. + Module is disabled by default can be turned on by setting SERIAL_MODULE_ENABLED = 1 in SerialPlugh.cpp. + ENCODING: binary undefined */ + meshtastic_PortNum_SERIAL_APP = 64, + /* STORE_FORWARD_APP (Work in Progress) + Maintained by Jm Casler (MC Hamster) : jm@casler.org + ENCODING: Protobuf */ + meshtastic_PortNum_STORE_FORWARD_APP = 65, + /* Optional port for messages for the range test module. + ENCODING: ASCII Plaintext + NOTE: This portnum traffic is not sent to the public MQTT starting at firmware version 2.2.9 */ + meshtastic_PortNum_RANGE_TEST_APP = 66, + /* Provides a format to send and receive telemetry data from the Meshtastic network. + Maintained by Charles Crossan (crossan007) : crossan007@gmail.com + ENCODING: Protobuf */ + meshtastic_PortNum_TELEMETRY_APP = 67, + /* Experimental tools for estimating node position without a GPS + Maintained by Github user a-f-G-U-C (a Meshtastic contributor) + Project files at https://github.com/a-f-G-U-C/Meshtastic-ZPS + ENCODING: arrays of int64 fields */ + meshtastic_PortNum_ZPS_APP = 68, + /* Used to let multiple instances of Linux native applications communicate + as if they did using their LoRa chip. + Maintained by GitHub user GUVWAF. + Project files at https://github.com/GUVWAF/Meshtasticator + ENCODING: Protobuf (?) */ + meshtastic_PortNum_SIMULATOR_APP = 69, + /* Provides a traceroute functionality to show the route a packet towards + a certain destination would take on the mesh. Contains a RouteDiscovery message as payload. + ENCODING: Protobuf */ + meshtastic_PortNum_TRACEROUTE_APP = 70, + /* Aggregates edge info for the network by sending out a list of each node's neighbors + ENCODING: Protobuf */ + meshtastic_PortNum_NEIGHBORINFO_APP = 71, + /* ATAK Plugin + Portnum for payloads from the official Meshtastic ATAK plugin */ + meshtastic_PortNum_ATAK_PLUGIN = 72, + /* Provides unencrypted information about a node for consumption by a map via MQTT */ + meshtastic_PortNum_MAP_REPORT_APP = 73, + /* PowerStress based monitoring support (for automated power consumption testing) */ + meshtastic_PortNum_POWERSTRESS_APP = 74, + /* Private applications should use portnums >= 256. + To simplify initial development and testing you can use "PRIVATE_APP" + in your code without needing to rebuild protobuf files (via [regen-protos.sh](https://github.com/meshtastic/firmware/blob/master/bin/regen-protos.sh)) */ + meshtastic_PortNum_PRIVATE_APP = 256, + /* ATAK Forwarder Module https://github.com/paulmandal/atak-forwarder + ENCODING: libcotshrink */ + meshtastic_PortNum_ATAK_FORWARDER = 257, + /* Currently we limit port nums to no higher than this value */ + meshtastic_PortNum_MAX = 511 +} meshtastic_PortNum; + +#ifdef __cplusplus +extern "C" { +#endif + +/* Helper constants for enums */ +#define _meshtastic_PortNum_MIN meshtastic_PortNum_UNKNOWN_APP +#define _meshtastic_PortNum_MAX meshtastic_PortNum_MAX +#define _meshtastic_PortNum_ARRAYSIZE ((meshtastic_PortNum)(meshtastic_PortNum_MAX+1)) + + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/mesh/generated/meshtastic/powermon.pb.cpp b/src/mesh/generated/meshtastic/powermon.pb.cpp new file mode 100644 index 0000000..6a9b755 --- /dev/null +++ b/src/mesh/generated/meshtastic/powermon.pb.cpp @@ -0,0 +1,19 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9 */ + +#include "meshtastic/powermon.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(meshtastic_PowerMon, meshtastic_PowerMon, AUTO) + + +PB_BIND(meshtastic_PowerStressMessage, meshtastic_PowerStressMessage, AUTO) + + + + + + + diff --git a/src/mesh/generated/meshtastic/powermon.pb.h b/src/mesh/generated/meshtastic/powermon.pb.h new file mode 100644 index 0000000..5add85b --- /dev/null +++ b/src/mesh/generated/meshtastic/powermon.pb.h @@ -0,0 +1,138 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9 */ + +#ifndef PB_MESHTASTIC_MESHTASTIC_POWERMON_PB_H_INCLUDED +#define PB_MESHTASTIC_MESHTASTIC_POWERMON_PB_H_INCLUDED +#include + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Enum definitions */ +/* Any significant power changing event in meshtastic should be tagged with a powermon state transition. +If you are making new meshtastic features feel free to add new entries at the end of this definition. */ +typedef enum _meshtastic_PowerMon_State { + meshtastic_PowerMon_State_None = 0, + meshtastic_PowerMon_State_CPU_DeepSleep = 1, + meshtastic_PowerMon_State_CPU_LightSleep = 2, + /* The external Vext1 power is on. Many boards have auxillary power rails that the CPU turns on only +occasionally. In cases where that rail has multiple devices on it we usually want to have logging on +the state of that rail as an independent record. +For instance on the Heltec Tracker 1.1 board, this rail is the power source for the GPS and screen. + +The log messages will be short and complete (see PowerMon.Event in the protobufs for details). +something like "S:PM:C,0x00001234,REASON" where the hex number is the bitmask of all current states. +(We use a bitmask for states so that if a log message gets lost it won't be fatal) */ + meshtastic_PowerMon_State_Vext1_On = 4, + meshtastic_PowerMon_State_Lora_RXOn = 8, + meshtastic_PowerMon_State_Lora_TXOn = 16, + meshtastic_PowerMon_State_Lora_RXActive = 32, + meshtastic_PowerMon_State_BT_On = 64, + meshtastic_PowerMon_State_LED_On = 128, + meshtastic_PowerMon_State_Screen_On = 256, + meshtastic_PowerMon_State_Screen_Drawing = 512, + meshtastic_PowerMon_State_Wifi_On = 1024, + /* GPS is actively trying to find our location +See GPSPowerState for more details */ + meshtastic_PowerMon_State_GPS_Active = 2048 +} meshtastic_PowerMon_State; + +/* What operation would we like the UUT to perform. +note: senders should probably set want_response in their request packets, so that they can know when the state +machine has started processing their request */ +typedef enum _meshtastic_PowerStressMessage_Opcode { + /* Unset/unused */ + meshtastic_PowerStressMessage_Opcode_UNSET = 0, + meshtastic_PowerStressMessage_Opcode_PRINT_INFO = 1, /* Print board version slog and send an ack that we are alive and ready to process commands */ + meshtastic_PowerStressMessage_Opcode_FORCE_QUIET = 2, /* Try to turn off all automatic processing of packets, screen, sleeping, etc (to make it easier to measure in isolation) */ + meshtastic_PowerStressMessage_Opcode_END_QUIET = 3, /* Stop powerstress processing - probably by just rebooting the board */ + meshtastic_PowerStressMessage_Opcode_SCREEN_ON = 16, /* Turn the screen on */ + meshtastic_PowerStressMessage_Opcode_SCREEN_OFF = 17, /* Turn the screen off */ + meshtastic_PowerStressMessage_Opcode_CPU_IDLE = 32, /* Let the CPU run but we assume mostly idling for num_seconds */ + meshtastic_PowerStressMessage_Opcode_CPU_DEEPSLEEP = 33, /* Force deep sleep for FIXME seconds */ + meshtastic_PowerStressMessage_Opcode_CPU_FULLON = 34, /* Spin the CPU as fast as possible for num_seconds */ + meshtastic_PowerStressMessage_Opcode_LED_ON = 48, /* Turn the LED on for num_seconds (and leave it on - for baseline power measurement purposes) */ + meshtastic_PowerStressMessage_Opcode_LED_OFF = 49, /* Force the LED off for num_seconds */ + meshtastic_PowerStressMessage_Opcode_LORA_OFF = 64, /* Completely turn off the LORA radio for num_seconds */ + meshtastic_PowerStressMessage_Opcode_LORA_TX = 65, /* Send Lora packets for num_seconds */ + meshtastic_PowerStressMessage_Opcode_LORA_RX = 66, /* Receive Lora packets for num_seconds (node will be mostly just listening, unless an external agent is helping stress this by sending packets on the current channel) */ + meshtastic_PowerStressMessage_Opcode_BT_OFF = 80, /* Turn off the BT radio for num_seconds */ + meshtastic_PowerStressMessage_Opcode_BT_ON = 81, /* Turn on the BT radio for num_seconds */ + meshtastic_PowerStressMessage_Opcode_WIFI_OFF = 96, /* Turn off the WIFI radio for num_seconds */ + meshtastic_PowerStressMessage_Opcode_WIFI_ON = 97, /* Turn on the WIFI radio for num_seconds */ + meshtastic_PowerStressMessage_Opcode_GPS_OFF = 112, /* Turn off the GPS radio for num_seconds */ + meshtastic_PowerStressMessage_Opcode_GPS_ON = 113 /* Turn on the GPS radio for num_seconds */ +} meshtastic_PowerStressMessage_Opcode; + +/* Struct definitions */ +/* Note: There are no 'PowerMon' messages normally in use (PowerMons are sent only as structured logs - slogs). +But we wrap our State enum in this message to effectively nest a namespace (without our linter yelling at us) */ +typedef struct _meshtastic_PowerMon { + char dummy_field; +} meshtastic_PowerMon; + +/* PowerStress testing support via the C++ PowerStress module */ +typedef struct _meshtastic_PowerStressMessage { + /* What type of HardwareMessage is this? */ + meshtastic_PowerStressMessage_Opcode cmd; + float num_seconds; +} meshtastic_PowerStressMessage; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Helper constants for enums */ +#define _meshtastic_PowerMon_State_MIN meshtastic_PowerMon_State_None +#define _meshtastic_PowerMon_State_MAX meshtastic_PowerMon_State_GPS_Active +#define _meshtastic_PowerMon_State_ARRAYSIZE ((meshtastic_PowerMon_State)(meshtastic_PowerMon_State_GPS_Active+1)) + +#define _meshtastic_PowerStressMessage_Opcode_MIN meshtastic_PowerStressMessage_Opcode_UNSET +#define _meshtastic_PowerStressMessage_Opcode_MAX meshtastic_PowerStressMessage_Opcode_GPS_ON +#define _meshtastic_PowerStressMessage_Opcode_ARRAYSIZE ((meshtastic_PowerStressMessage_Opcode)(meshtastic_PowerStressMessage_Opcode_GPS_ON+1)) + + +#define meshtastic_PowerStressMessage_cmd_ENUMTYPE meshtastic_PowerStressMessage_Opcode + + +/* Initializer values for message structs */ +#define meshtastic_PowerMon_init_default {0} +#define meshtastic_PowerStressMessage_init_default {_meshtastic_PowerStressMessage_Opcode_MIN, 0} +#define meshtastic_PowerMon_init_zero {0} +#define meshtastic_PowerStressMessage_init_zero {_meshtastic_PowerStressMessage_Opcode_MIN, 0} + +/* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_PowerStressMessage_cmd_tag 1 +#define meshtastic_PowerStressMessage_num_seconds_tag 2 + +/* Struct field encoding specification for nanopb */ +#define meshtastic_PowerMon_FIELDLIST(X, a) \ + +#define meshtastic_PowerMon_CALLBACK NULL +#define meshtastic_PowerMon_DEFAULT NULL + +#define meshtastic_PowerStressMessage_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, cmd, 1) \ +X(a, STATIC, SINGULAR, FLOAT, num_seconds, 2) +#define meshtastic_PowerStressMessage_CALLBACK NULL +#define meshtastic_PowerStressMessage_DEFAULT NULL + +extern const pb_msgdesc_t meshtastic_PowerMon_msg; +extern const pb_msgdesc_t meshtastic_PowerStressMessage_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define meshtastic_PowerMon_fields &meshtastic_PowerMon_msg +#define meshtastic_PowerStressMessage_fields &meshtastic_PowerStressMessage_msg + +/* Maximum encoded size of messages (where known) */ +#define MESHTASTIC_MESHTASTIC_POWERMON_PB_H_MAX_SIZE meshtastic_PowerStressMessage_size +#define meshtastic_PowerMon_size 0 +#define meshtastic_PowerStressMessage_size 7 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/mesh/generated/meshtastic/remote_hardware.pb.cpp b/src/mesh/generated/meshtastic/remote_hardware.pb.cpp new file mode 100644 index 0000000..239950e --- /dev/null +++ b/src/mesh/generated/meshtastic/remote_hardware.pb.cpp @@ -0,0 +1,14 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9 */ + +#include "meshtastic/remote_hardware.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(meshtastic_HardwareMessage, meshtastic_HardwareMessage, AUTO) + + + + + diff --git a/src/mesh/generated/meshtastic/remote_hardware.pb.h b/src/mesh/generated/meshtastic/remote_hardware.pb.h new file mode 100644 index 0000000..ade250e --- /dev/null +++ b/src/mesh/generated/meshtastic/remote_hardware.pb.h @@ -0,0 +1,94 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9 */ + +#ifndef PB_MESHTASTIC_MESHTASTIC_REMOTE_HARDWARE_PB_H_INCLUDED +#define PB_MESHTASTIC_MESHTASTIC_REMOTE_HARDWARE_PB_H_INCLUDED +#include + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Enum definitions */ +/* TODO: REPLACE */ +typedef enum _meshtastic_HardwareMessage_Type { + /* Unset/unused */ + meshtastic_HardwareMessage_Type_UNSET = 0, + /* Set gpio gpios based on gpio_mask/gpio_value */ + meshtastic_HardwareMessage_Type_WRITE_GPIOS = 1, + /* We are now interested in watching the gpio_mask gpios. + If the selected gpios change, please broadcast GPIOS_CHANGED. + Will implicitly change the gpios requested to be INPUT gpios. */ + meshtastic_HardwareMessage_Type_WATCH_GPIOS = 2, + /* The gpios listed in gpio_mask have changed, the new values are listed in gpio_value */ + meshtastic_HardwareMessage_Type_GPIOS_CHANGED = 3, + /* Read the gpios specified in gpio_mask, send back a READ_GPIOS_REPLY reply with gpio_value populated */ + meshtastic_HardwareMessage_Type_READ_GPIOS = 4, + /* A reply to READ_GPIOS. gpio_mask and gpio_value will be populated */ + meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY = 5 +} meshtastic_HardwareMessage_Type; + +/* Struct definitions */ +/* An example app to show off the module system. This message is used for + REMOTE_HARDWARE_APP PortNums. + Also provides easy remote access to any GPIO. + In the future other remote hardware operations can be added based on user interest + (i.e. serial output, spi/i2c input/output). + FIXME - currently this feature is turned on by default which is dangerous + because no security yet (beyond the channel mechanism). + It should be off by default and then protected based on some TBD mechanism + (a special channel once multichannel support is included?) */ +typedef struct _meshtastic_HardwareMessage { + /* What type of HardwareMessage is this? */ + meshtastic_HardwareMessage_Type type; + /* What gpios are we changing. Not used for all MessageTypes, see MessageType for details */ + uint64_t gpio_mask; + /* For gpios that were listed in gpio_mask as valid, what are the signal levels for those gpios. + Not used for all MessageTypes, see MessageType for details */ + uint64_t gpio_value; +} meshtastic_HardwareMessage; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Helper constants for enums */ +#define _meshtastic_HardwareMessage_Type_MIN meshtastic_HardwareMessage_Type_UNSET +#define _meshtastic_HardwareMessage_Type_MAX meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY +#define _meshtastic_HardwareMessage_Type_ARRAYSIZE ((meshtastic_HardwareMessage_Type)(meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY+1)) + +#define meshtastic_HardwareMessage_type_ENUMTYPE meshtastic_HardwareMessage_Type + + +/* Initializer values for message structs */ +#define meshtastic_HardwareMessage_init_default {_meshtastic_HardwareMessage_Type_MIN, 0, 0} +#define meshtastic_HardwareMessage_init_zero {_meshtastic_HardwareMessage_Type_MIN, 0, 0} + +/* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_HardwareMessage_type_tag 1 +#define meshtastic_HardwareMessage_gpio_mask_tag 2 +#define meshtastic_HardwareMessage_gpio_value_tag 3 + +/* Struct field encoding specification for nanopb */ +#define meshtastic_HardwareMessage_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, type, 1) \ +X(a, STATIC, SINGULAR, UINT64, gpio_mask, 2) \ +X(a, STATIC, SINGULAR, UINT64, gpio_value, 3) +#define meshtastic_HardwareMessage_CALLBACK NULL +#define meshtastic_HardwareMessage_DEFAULT NULL + +extern const pb_msgdesc_t meshtastic_HardwareMessage_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define meshtastic_HardwareMessage_fields &meshtastic_HardwareMessage_msg + +/* Maximum encoded size of messages (where known) */ +#define MESHTASTIC_MESHTASTIC_REMOTE_HARDWARE_PB_H_MAX_SIZE meshtastic_HardwareMessage_size +#define meshtastic_HardwareMessage_size 24 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/mesh/generated/meshtastic/rtttl.pb.cpp b/src/mesh/generated/meshtastic/rtttl.pb.cpp new file mode 100644 index 0000000..61ad8b7 --- /dev/null +++ b/src/mesh/generated/meshtastic/rtttl.pb.cpp @@ -0,0 +1,12 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9 */ + +#include "meshtastic/rtttl.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(meshtastic_RTTTLConfig, meshtastic_RTTTLConfig, AUTO) + + + diff --git a/src/mesh/generated/meshtastic/rtttl.pb.h b/src/mesh/generated/meshtastic/rtttl.pb.h new file mode 100644 index 0000000..0572265 --- /dev/null +++ b/src/mesh/generated/meshtastic/rtttl.pb.h @@ -0,0 +1,50 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9 */ + +#ifndef PB_MESHTASTIC_MESHTASTIC_RTTTL_PB_H_INCLUDED +#define PB_MESHTASTIC_MESHTASTIC_RTTTL_PB_H_INCLUDED +#include + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Struct definitions */ +/* Canned message module configuration. */ +typedef struct _meshtastic_RTTTLConfig { + /* Ringtone for PWM Buzzer in RTTTL Format. */ + char ringtone[231]; +} meshtastic_RTTTLConfig; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initializer values for message structs */ +#define meshtastic_RTTTLConfig_init_default {""} +#define meshtastic_RTTTLConfig_init_zero {""} + +/* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_RTTTLConfig_ringtone_tag 1 + +/* Struct field encoding specification for nanopb */ +#define meshtastic_RTTTLConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, STRING, ringtone, 1) +#define meshtastic_RTTTLConfig_CALLBACK NULL +#define meshtastic_RTTTLConfig_DEFAULT NULL + +extern const pb_msgdesc_t meshtastic_RTTTLConfig_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define meshtastic_RTTTLConfig_fields &meshtastic_RTTTLConfig_msg + +/* Maximum encoded size of messages (where known) */ +#define MESHTASTIC_MESHTASTIC_RTTTL_PB_H_MAX_SIZE meshtastic_RTTTLConfig_size +#define meshtastic_RTTTLConfig_size 233 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/mesh/generated/meshtastic/storeforward.pb.cpp b/src/mesh/generated/meshtastic/storeforward.pb.cpp new file mode 100644 index 0000000..71a232b --- /dev/null +++ b/src/mesh/generated/meshtastic/storeforward.pb.cpp @@ -0,0 +1,23 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9 */ + +#include "meshtastic/storeforward.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(meshtastic_StoreAndForward, meshtastic_StoreAndForward, AUTO) + + +PB_BIND(meshtastic_StoreAndForward_Statistics, meshtastic_StoreAndForward_Statistics, AUTO) + + +PB_BIND(meshtastic_StoreAndForward_History, meshtastic_StoreAndForward_History, AUTO) + + +PB_BIND(meshtastic_StoreAndForward_Heartbeat, meshtastic_StoreAndForward_Heartbeat, AUTO) + + + + + diff --git a/src/mesh/generated/meshtastic/storeforward.pb.h b/src/mesh/generated/meshtastic/storeforward.pb.h new file mode 100644 index 0000000..71f2fca --- /dev/null +++ b/src/mesh/generated/meshtastic/storeforward.pb.h @@ -0,0 +1,220 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9 */ + +#ifndef PB_MESHTASTIC_MESHTASTIC_STOREFORWARD_PB_H_INCLUDED +#define PB_MESHTASTIC_MESHTASTIC_STOREFORWARD_PB_H_INCLUDED +#include + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Enum definitions */ +/* 001 - 063 = From Router + 064 - 127 = From Client */ +typedef enum _meshtastic_StoreAndForward_RequestResponse { + /* Unset/unused */ + meshtastic_StoreAndForward_RequestResponse_UNSET = 0, + /* Router is an in error state. */ + meshtastic_StoreAndForward_RequestResponse_ROUTER_ERROR = 1, + /* Router heartbeat */ + meshtastic_StoreAndForward_RequestResponse_ROUTER_HEARTBEAT = 2, + /* Router has requested the client respond. This can work as a + "are you there" message. */ + meshtastic_StoreAndForward_RequestResponse_ROUTER_PING = 3, + /* The response to a "Ping" */ + meshtastic_StoreAndForward_RequestResponse_ROUTER_PONG = 4, + /* Router is currently busy. Please try again later. */ + meshtastic_StoreAndForward_RequestResponse_ROUTER_BUSY = 5, + /* Router is responding to a request for history. */ + meshtastic_StoreAndForward_RequestResponse_ROUTER_HISTORY = 6, + /* Router is responding to a request for stats. */ + meshtastic_StoreAndForward_RequestResponse_ROUTER_STATS = 7, + /* Router sends a text message from its history that was a direct message. */ + meshtastic_StoreAndForward_RequestResponse_ROUTER_TEXT_DIRECT = 8, + /* Router sends a text message from its history that was a broadcast. */ + meshtastic_StoreAndForward_RequestResponse_ROUTER_TEXT_BROADCAST = 9, + /* Client is an in error state. */ + meshtastic_StoreAndForward_RequestResponse_CLIENT_ERROR = 64, + /* Client has requested a replay from the router. */ + meshtastic_StoreAndForward_RequestResponse_CLIENT_HISTORY = 65, + /* Client has requested stats from the router. */ + meshtastic_StoreAndForward_RequestResponse_CLIENT_STATS = 66, + /* Client has requested the router respond. This can work as a + "are you there" message. */ + meshtastic_StoreAndForward_RequestResponse_CLIENT_PING = 67, + /* The response to a "Ping" */ + meshtastic_StoreAndForward_RequestResponse_CLIENT_PONG = 68, + /* Client has requested that the router abort processing the client's request */ + meshtastic_StoreAndForward_RequestResponse_CLIENT_ABORT = 106 +} meshtastic_StoreAndForward_RequestResponse; + +/* Struct definitions */ +/* TODO: REPLACE */ +typedef struct _meshtastic_StoreAndForward_Statistics { + /* Number of messages we have ever seen */ + uint32_t messages_total; + /* Number of messages we have currently saved our history. */ + uint32_t messages_saved; + /* Maximum number of messages we will save */ + uint32_t messages_max; + /* Router uptime in seconds */ + uint32_t up_time; + /* Number of times any client sent a request to the S&F. */ + uint32_t requests; + /* Number of times the history was requested. */ + uint32_t requests_history; + /* Is the heartbeat enabled on the server? */ + bool heartbeat; + /* Maximum number of messages the server will return. */ + uint32_t return_max; + /* Maximum history window in minutes the server will return messages from. */ + uint32_t return_window; +} meshtastic_StoreAndForward_Statistics; + +/* TODO: REPLACE */ +typedef struct _meshtastic_StoreAndForward_History { + /* Number of that will be sent to the client */ + uint32_t history_messages; + /* The window of messages that was used to filter the history client requested */ + uint32_t window; + /* Index in the packet history of the last message sent in a previous request to the server. + Will be sent to the client before sending the history and can be set in a subsequent request to avoid getting packets the server already sent to the client. */ + uint32_t last_request; +} meshtastic_StoreAndForward_History; + +/* TODO: REPLACE */ +typedef struct _meshtastic_StoreAndForward_Heartbeat { + /* Period in seconds that the heartbeat is sent out that will be sent to the client */ + uint32_t period; + /* If set, this is not the primary Store & Forward router on the mesh */ + uint32_t secondary; +} meshtastic_StoreAndForward_Heartbeat; + +typedef PB_BYTES_ARRAY_T(237) meshtastic_StoreAndForward_text_t; +/* TODO: REPLACE */ +typedef struct _meshtastic_StoreAndForward { + /* TODO: REPLACE */ + meshtastic_StoreAndForward_RequestResponse rr; + pb_size_t which_variant; + union { + /* TODO: REPLACE */ + meshtastic_StoreAndForward_Statistics stats; + /* TODO: REPLACE */ + meshtastic_StoreAndForward_History history; + /* TODO: REPLACE */ + meshtastic_StoreAndForward_Heartbeat heartbeat; + /* Text from history message. */ + meshtastic_StoreAndForward_text_t text; + } variant; +} meshtastic_StoreAndForward; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Helper constants for enums */ +#define _meshtastic_StoreAndForward_RequestResponse_MIN meshtastic_StoreAndForward_RequestResponse_UNSET +#define _meshtastic_StoreAndForward_RequestResponse_MAX meshtastic_StoreAndForward_RequestResponse_CLIENT_ABORT +#define _meshtastic_StoreAndForward_RequestResponse_ARRAYSIZE ((meshtastic_StoreAndForward_RequestResponse)(meshtastic_StoreAndForward_RequestResponse_CLIENT_ABORT+1)) + +#define meshtastic_StoreAndForward_rr_ENUMTYPE meshtastic_StoreAndForward_RequestResponse + + + + + +/* Initializer values for message structs */ +#define meshtastic_StoreAndForward_init_default {_meshtastic_StoreAndForward_RequestResponse_MIN, 0, {meshtastic_StoreAndForward_Statistics_init_default}} +#define meshtastic_StoreAndForward_Statistics_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0} +#define meshtastic_StoreAndForward_History_init_default {0, 0, 0} +#define meshtastic_StoreAndForward_Heartbeat_init_default {0, 0} +#define meshtastic_StoreAndForward_init_zero {_meshtastic_StoreAndForward_RequestResponse_MIN, 0, {meshtastic_StoreAndForward_Statistics_init_zero}} +#define meshtastic_StoreAndForward_Statistics_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0} +#define meshtastic_StoreAndForward_History_init_zero {0, 0, 0} +#define meshtastic_StoreAndForward_Heartbeat_init_zero {0, 0} + +/* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_StoreAndForward_Statistics_messages_total_tag 1 +#define meshtastic_StoreAndForward_Statistics_messages_saved_tag 2 +#define meshtastic_StoreAndForward_Statistics_messages_max_tag 3 +#define meshtastic_StoreAndForward_Statistics_up_time_tag 4 +#define meshtastic_StoreAndForward_Statistics_requests_tag 5 +#define meshtastic_StoreAndForward_Statistics_requests_history_tag 6 +#define meshtastic_StoreAndForward_Statistics_heartbeat_tag 7 +#define meshtastic_StoreAndForward_Statistics_return_max_tag 8 +#define meshtastic_StoreAndForward_Statistics_return_window_tag 9 +#define meshtastic_StoreAndForward_History_history_messages_tag 1 +#define meshtastic_StoreAndForward_History_window_tag 2 +#define meshtastic_StoreAndForward_History_last_request_tag 3 +#define meshtastic_StoreAndForward_Heartbeat_period_tag 1 +#define meshtastic_StoreAndForward_Heartbeat_secondary_tag 2 +#define meshtastic_StoreAndForward_rr_tag 1 +#define meshtastic_StoreAndForward_stats_tag 2 +#define meshtastic_StoreAndForward_history_tag 3 +#define meshtastic_StoreAndForward_heartbeat_tag 4 +#define meshtastic_StoreAndForward_text_tag 5 + +/* Struct field encoding specification for nanopb */ +#define meshtastic_StoreAndForward_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, rr, 1) \ +X(a, STATIC, ONEOF, MESSAGE, (variant,stats,variant.stats), 2) \ +X(a, STATIC, ONEOF, MESSAGE, (variant,history,variant.history), 3) \ +X(a, STATIC, ONEOF, MESSAGE, (variant,heartbeat,variant.heartbeat), 4) \ +X(a, STATIC, ONEOF, BYTES, (variant,text,variant.text), 5) +#define meshtastic_StoreAndForward_CALLBACK NULL +#define meshtastic_StoreAndForward_DEFAULT NULL +#define meshtastic_StoreAndForward_variant_stats_MSGTYPE meshtastic_StoreAndForward_Statistics +#define meshtastic_StoreAndForward_variant_history_MSGTYPE meshtastic_StoreAndForward_History +#define meshtastic_StoreAndForward_variant_heartbeat_MSGTYPE meshtastic_StoreAndForward_Heartbeat + +#define meshtastic_StoreAndForward_Statistics_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, messages_total, 1) \ +X(a, STATIC, SINGULAR, UINT32, messages_saved, 2) \ +X(a, STATIC, SINGULAR, UINT32, messages_max, 3) \ +X(a, STATIC, SINGULAR, UINT32, up_time, 4) \ +X(a, STATIC, SINGULAR, UINT32, requests, 5) \ +X(a, STATIC, SINGULAR, UINT32, requests_history, 6) \ +X(a, STATIC, SINGULAR, BOOL, heartbeat, 7) \ +X(a, STATIC, SINGULAR, UINT32, return_max, 8) \ +X(a, STATIC, SINGULAR, UINT32, return_window, 9) +#define meshtastic_StoreAndForward_Statistics_CALLBACK NULL +#define meshtastic_StoreAndForward_Statistics_DEFAULT NULL + +#define meshtastic_StoreAndForward_History_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, history_messages, 1) \ +X(a, STATIC, SINGULAR, UINT32, window, 2) \ +X(a, STATIC, SINGULAR, UINT32, last_request, 3) +#define meshtastic_StoreAndForward_History_CALLBACK NULL +#define meshtastic_StoreAndForward_History_DEFAULT NULL + +#define meshtastic_StoreAndForward_Heartbeat_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, period, 1) \ +X(a, STATIC, SINGULAR, UINT32, secondary, 2) +#define meshtastic_StoreAndForward_Heartbeat_CALLBACK NULL +#define meshtastic_StoreAndForward_Heartbeat_DEFAULT NULL + +extern const pb_msgdesc_t meshtastic_StoreAndForward_msg; +extern const pb_msgdesc_t meshtastic_StoreAndForward_Statistics_msg; +extern const pb_msgdesc_t meshtastic_StoreAndForward_History_msg; +extern const pb_msgdesc_t meshtastic_StoreAndForward_Heartbeat_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define meshtastic_StoreAndForward_fields &meshtastic_StoreAndForward_msg +#define meshtastic_StoreAndForward_Statistics_fields &meshtastic_StoreAndForward_Statistics_msg +#define meshtastic_StoreAndForward_History_fields &meshtastic_StoreAndForward_History_msg +#define meshtastic_StoreAndForward_Heartbeat_fields &meshtastic_StoreAndForward_Heartbeat_msg + +/* Maximum encoded size of messages (where known) */ +#define MESHTASTIC_MESHTASTIC_STOREFORWARD_PB_H_MAX_SIZE meshtastic_StoreAndForward_size +#define meshtastic_StoreAndForward_Heartbeat_size 12 +#define meshtastic_StoreAndForward_History_size 18 +#define meshtastic_StoreAndForward_Statistics_size 50 +#define meshtastic_StoreAndForward_size 242 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/mesh/generated/meshtastic/telemetry.pb.cpp b/src/mesh/generated/meshtastic/telemetry.pb.cpp new file mode 100644 index 0000000..f6d39da --- /dev/null +++ b/src/mesh/generated/meshtastic/telemetry.pb.cpp @@ -0,0 +1,35 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9 */ + +#include "meshtastic/telemetry.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(meshtastic_DeviceMetrics, meshtastic_DeviceMetrics, AUTO) + + +PB_BIND(meshtastic_EnvironmentMetrics, meshtastic_EnvironmentMetrics, AUTO) + + +PB_BIND(meshtastic_PowerMetrics, meshtastic_PowerMetrics, AUTO) + + +PB_BIND(meshtastic_AirQualityMetrics, meshtastic_AirQualityMetrics, AUTO) + + +PB_BIND(meshtastic_LocalStats, meshtastic_LocalStats, AUTO) + + +PB_BIND(meshtastic_HealthMetrics, meshtastic_HealthMetrics, AUTO) + + +PB_BIND(meshtastic_Telemetry, meshtastic_Telemetry, AUTO) + + +PB_BIND(meshtastic_Nau7802Config, meshtastic_Nau7802Config, AUTO) + + + + + diff --git a/src/mesh/generated/meshtastic/telemetry.pb.h b/src/mesh/generated/meshtastic/telemetry.pb.h new file mode 100644 index 0000000..309c01d --- /dev/null +++ b/src/mesh/generated/meshtastic/telemetry.pb.h @@ -0,0 +1,535 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9 */ + +#ifndef PB_MESHTASTIC_MESHTASTIC_TELEMETRY_PB_H_INCLUDED +#define PB_MESHTASTIC_MESHTASTIC_TELEMETRY_PB_H_INCLUDED +#include + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Enum definitions */ +/* Supported I2C Sensors for telemetry in Meshtastic */ +typedef enum _meshtastic_TelemetrySensorType { + /* No external telemetry sensor explicitly set */ + meshtastic_TelemetrySensorType_SENSOR_UNSET = 0, + /* High accuracy temperature, pressure, humidity */ + meshtastic_TelemetrySensorType_BME280 = 1, + /* High accuracy temperature, pressure, humidity, and air resistance */ + meshtastic_TelemetrySensorType_BME680 = 2, + /* Very high accuracy temperature */ + meshtastic_TelemetrySensorType_MCP9808 = 3, + /* Moderate accuracy current and voltage */ + meshtastic_TelemetrySensorType_INA260 = 4, + /* Moderate accuracy current and voltage */ + meshtastic_TelemetrySensorType_INA219 = 5, + /* High accuracy temperature and pressure */ + meshtastic_TelemetrySensorType_BMP280 = 6, + /* High accuracy temperature and humidity */ + meshtastic_TelemetrySensorType_SHTC3 = 7, + /* High accuracy pressure */ + meshtastic_TelemetrySensorType_LPS22 = 8, + /* 3-Axis magnetic sensor */ + meshtastic_TelemetrySensorType_QMC6310 = 9, + /* 6-Axis inertial measurement sensor */ + meshtastic_TelemetrySensorType_QMI8658 = 10, + /* 3-Axis magnetic sensor */ + meshtastic_TelemetrySensorType_QMC5883L = 11, + /* High accuracy temperature and humidity */ + meshtastic_TelemetrySensorType_SHT31 = 12, + /* PM2.5 air quality sensor */ + meshtastic_TelemetrySensorType_PMSA003I = 13, + /* INA3221 3 Channel Voltage / Current Sensor */ + meshtastic_TelemetrySensorType_INA3221 = 14, + /* BMP085/BMP180 High accuracy temperature and pressure (older Version of BMP280) */ + meshtastic_TelemetrySensorType_BMP085 = 15, + /* RCWL-9620 Doppler Radar Distance Sensor, used for water level detection */ + meshtastic_TelemetrySensorType_RCWL9620 = 16, + /* Sensirion High accuracy temperature and humidity */ + meshtastic_TelemetrySensorType_SHT4X = 17, + /* VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor. */ + meshtastic_TelemetrySensorType_VEML7700 = 18, + /* MLX90632 non-contact IR temperature sensor. */ + meshtastic_TelemetrySensorType_MLX90632 = 19, + /* TI OPT3001 Ambient Light Sensor */ + meshtastic_TelemetrySensorType_OPT3001 = 20, + /* Lite On LTR-390UV-01 UV Light Sensor */ + meshtastic_TelemetrySensorType_LTR390UV = 21, + /* AMS TSL25911FN RGB Light Sensor */ + meshtastic_TelemetrySensorType_TSL25911FN = 22, + /* AHT10 Integrated temperature and humidity sensor */ + meshtastic_TelemetrySensorType_AHT10 = 23, + /* DFRobot Lark Weather station (temperature, humidity, pressure, wind speed and direction) */ + meshtastic_TelemetrySensorType_DFROBOT_LARK = 24, + /* NAU7802 Scale Chip or compatible */ + meshtastic_TelemetrySensorType_NAU7802 = 25, + /* BMP3XX High accuracy temperature and pressure */ + meshtastic_TelemetrySensorType_BMP3XX = 26, + /* ICM-20948 9-Axis digital motion processor */ + meshtastic_TelemetrySensorType_ICM20948 = 27, + /* MAX17048 1S lipo battery sensor (voltage, state of charge, time to go) */ + meshtastic_TelemetrySensorType_MAX17048 = 28, + /* Custom I2C sensor implementation based on https://github.com/meshtastic/i2c-sensor */ + meshtastic_TelemetrySensorType_CUSTOM_SENSOR = 29, + /* MAX30102 Pulse Oximeter and Heart-Rate Sensor */ + meshtastic_TelemetrySensorType_MAX30102 = 30, + /* MLX90614 non-contact IR temperature sensor */ + meshtastic_TelemetrySensorType_MLX90614 = 31, + /* SCD40/SCD41 CO2, humidity, temperature sensor */ + meshtastic_TelemetrySensorType_SCD4X = 32 +} meshtastic_TelemetrySensorType; + +/* Struct definitions */ +/* Key native device metrics such as battery level */ +typedef struct _meshtastic_DeviceMetrics { + /* 0-100 (>100 means powered) */ + bool has_battery_level; + uint32_t battery_level; + /* Voltage measured */ + bool has_voltage; + float voltage; + /* Utilization for the current channel, including well formed TX, RX and malformed RX (aka noise). */ + bool has_channel_utilization; + float channel_utilization; + /* Percent of airtime for transmission used within the last hour. */ + bool has_air_util_tx; + float air_util_tx; + /* How long the device has been running since the last reboot (in seconds) */ + bool has_uptime_seconds; + uint32_t uptime_seconds; +} meshtastic_DeviceMetrics; + +/* Weather station or other environmental metrics */ +typedef struct _meshtastic_EnvironmentMetrics { + /* Temperature measured */ + bool has_temperature; + float temperature; + /* Relative humidity percent measured */ + bool has_relative_humidity; + float relative_humidity; + /* Barometric pressure in hPA measured */ + bool has_barometric_pressure; + float barometric_pressure; + /* Gas resistance in MOhm measured */ + bool has_gas_resistance; + float gas_resistance; + /* Voltage measured (To be depreciated in favor of PowerMetrics in Meshtastic 3.x) */ + bool has_voltage; + float voltage; + /* Current measured (To be depreciated in favor of PowerMetrics in Meshtastic 3.x) */ + bool has_current; + float current; + /* relative scale IAQ value as measured by Bosch BME680 . value 0-500. + Belongs to Air Quality but is not particle but VOC measurement. Other VOC values can also be put in here. */ + bool has_iaq; + uint16_t iaq; + /* RCWL9620 Doppler Radar Distance Sensor, used for water level detection. Float value in mm. */ + bool has_distance; + float distance; + /* VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor. */ + bool has_lux; + float lux; + /* VEML7700 high accuracy white light(irradiance) not calibrated digital 16-bit resolution sensor. */ + bool has_white_lux; + float white_lux; + /* Infrared lux */ + bool has_ir_lux; + float ir_lux; + /* Ultraviolet lux */ + bool has_uv_lux; + float uv_lux; + /* Wind direction in degrees + 0 degrees = North, 90 = East, etc... */ + bool has_wind_direction; + uint16_t wind_direction; + /* Wind speed in m/s */ + bool has_wind_speed; + float wind_speed; + /* Weight in KG */ + bool has_weight; + float weight; + /* Wind gust in m/s */ + bool has_wind_gust; + float wind_gust; + /* Wind lull in m/s */ + bool has_wind_lull; + float wind_lull; +} meshtastic_EnvironmentMetrics; + +/* Power Metrics (voltage / current / etc) */ +typedef struct _meshtastic_PowerMetrics { + /* Voltage (Ch1) */ + bool has_ch1_voltage; + float ch1_voltage; + /* Current (Ch1) */ + bool has_ch1_current; + float ch1_current; + /* Voltage (Ch2) */ + bool has_ch2_voltage; + float ch2_voltage; + /* Current (Ch2) */ + bool has_ch2_current; + float ch2_current; + /* Voltage (Ch3) */ + bool has_ch3_voltage; + float ch3_voltage; + /* Current (Ch3) */ + bool has_ch3_current; + float ch3_current; +} meshtastic_PowerMetrics; + +/* Air quality metrics */ +typedef struct _meshtastic_AirQualityMetrics { + /* Concentration Units Standard PM1.0 */ + bool has_pm10_standard; + uint32_t pm10_standard; + /* Concentration Units Standard PM2.5 */ + bool has_pm25_standard; + uint32_t pm25_standard; + /* Concentration Units Standard PM10.0 */ + bool has_pm100_standard; + uint32_t pm100_standard; + /* Concentration Units Environmental PM1.0 */ + bool has_pm10_environmental; + uint32_t pm10_environmental; + /* Concentration Units Environmental PM2.5 */ + bool has_pm25_environmental; + uint32_t pm25_environmental; + /* Concentration Units Environmental PM10.0 */ + bool has_pm100_environmental; + uint32_t pm100_environmental; + /* 0.3um Particle Count */ + bool has_particles_03um; + uint32_t particles_03um; + /* 0.5um Particle Count */ + bool has_particles_05um; + uint32_t particles_05um; + /* 1.0um Particle Count */ + bool has_particles_10um; + uint32_t particles_10um; + /* 2.5um Particle Count */ + bool has_particles_25um; + uint32_t particles_25um; + /* 5.0um Particle Count */ + bool has_particles_50um; + uint32_t particles_50um; + /* 10.0um Particle Count */ + bool has_particles_100um; + uint32_t particles_100um; + /* 10.0um Particle Count */ + bool has_co2; + uint32_t co2; +} meshtastic_AirQualityMetrics; + +/* Local device mesh statistics */ +typedef struct _meshtastic_LocalStats { + /* How long the device has been running since the last reboot (in seconds) */ + uint32_t uptime_seconds; + /* Utilization for the current channel, including well formed TX, RX and malformed RX (aka noise). */ + float channel_utilization; + /* Percent of airtime for transmission used within the last hour. */ + float air_util_tx; + /* Number of packets sent */ + uint32_t num_packets_tx; + /* Number of packets received (both good and bad) */ + uint32_t num_packets_rx; + /* Number of packets received that are malformed or violate the protocol */ + uint32_t num_packets_rx_bad; + /* Number of nodes online (in the past 2 hours) */ + uint16_t num_online_nodes; + /* Number of nodes total */ + uint16_t num_total_nodes; + /* Number of received packets that were duplicates (due to multiple nodes relaying). + If this number is high, there are nodes in the mesh relaying packets when it's unnecessary, for example due to the ROUTER/REPEATER role. */ + uint32_t num_rx_dupe; + /* Number of packets we transmitted that were a relay for others (not originating from ourselves). */ + uint32_t num_tx_relay; + /* Number of times we canceled a packet to be relayed, because someone else did it before us. + This will always be zero for ROUTERs/REPEATERs. If this number is high, some other node(s) is/are relaying faster than you. */ + uint32_t num_tx_relay_canceled; +} meshtastic_LocalStats; + +/* Health telemetry metrics */ +typedef struct _meshtastic_HealthMetrics { + /* Heart rate (beats per minute) */ + bool has_heart_bpm; + uint8_t heart_bpm; + /* SpO2 (blood oxygen saturation) level */ + bool has_spO2; + uint8_t spO2; + /* Body temperature in degrees Celsius */ + bool has_temperature; + float temperature; +} meshtastic_HealthMetrics; + +/* Types of Measurements the telemetry module is equipped to handle */ +typedef struct _meshtastic_Telemetry { + /* Seconds since 1970 - or 0 for unknown/unset */ + uint32_t time; + pb_size_t which_variant; + union { + /* Key native device metrics such as battery level */ + meshtastic_DeviceMetrics device_metrics; + /* Weather station or other environmental metrics */ + meshtastic_EnvironmentMetrics environment_metrics; + /* Air quality metrics */ + meshtastic_AirQualityMetrics air_quality_metrics; + /* Power Metrics */ + meshtastic_PowerMetrics power_metrics; + /* Local device mesh statistics */ + meshtastic_LocalStats local_stats; + /* Health telemetry metrics */ + meshtastic_HealthMetrics health_metrics; + } variant; +} meshtastic_Telemetry; + +/* NAU7802 Telemetry configuration, for saving to flash */ +typedef struct _meshtastic_Nau7802Config { + /* The offset setting for the NAU7802 */ + int32_t zeroOffset; + /* The calibration factor for the NAU7802 */ + float calibrationFactor; +} meshtastic_Nau7802Config; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Helper constants for enums */ +#define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET +#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_SCD4X +#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_SCD4X+1)) + + + + + + + + + + +/* Initializer values for message structs */ +#define meshtastic_DeviceMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0} +#define meshtastic_EnvironmentMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} +#define meshtastic_PowerMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} +#define meshtastic_AirQualityMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} +#define meshtastic_LocalStats_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +#define meshtastic_HealthMetrics_init_default {false, 0, false, 0, false, 0} +#define meshtastic_Telemetry_init_default {0, 0, {meshtastic_DeviceMetrics_init_default}} +#define meshtastic_Nau7802Config_init_default {0, 0} +#define meshtastic_DeviceMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0} +#define meshtastic_EnvironmentMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} +#define meshtastic_PowerMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} +#define meshtastic_AirQualityMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} +#define meshtastic_LocalStats_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +#define meshtastic_HealthMetrics_init_zero {false, 0, false, 0, false, 0} +#define meshtastic_Telemetry_init_zero {0, 0, {meshtastic_DeviceMetrics_init_zero}} +#define meshtastic_Nau7802Config_init_zero {0, 0} + +/* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_DeviceMetrics_battery_level_tag 1 +#define meshtastic_DeviceMetrics_voltage_tag 2 +#define meshtastic_DeviceMetrics_channel_utilization_tag 3 +#define meshtastic_DeviceMetrics_air_util_tx_tag 4 +#define meshtastic_DeviceMetrics_uptime_seconds_tag 5 +#define meshtastic_EnvironmentMetrics_temperature_tag 1 +#define meshtastic_EnvironmentMetrics_relative_humidity_tag 2 +#define meshtastic_EnvironmentMetrics_barometric_pressure_tag 3 +#define meshtastic_EnvironmentMetrics_gas_resistance_tag 4 +#define meshtastic_EnvironmentMetrics_voltage_tag 5 +#define meshtastic_EnvironmentMetrics_current_tag 6 +#define meshtastic_EnvironmentMetrics_iaq_tag 7 +#define meshtastic_EnvironmentMetrics_distance_tag 8 +#define meshtastic_EnvironmentMetrics_lux_tag 9 +#define meshtastic_EnvironmentMetrics_white_lux_tag 10 +#define meshtastic_EnvironmentMetrics_ir_lux_tag 11 +#define meshtastic_EnvironmentMetrics_uv_lux_tag 12 +#define meshtastic_EnvironmentMetrics_wind_direction_tag 13 +#define meshtastic_EnvironmentMetrics_wind_speed_tag 14 +#define meshtastic_EnvironmentMetrics_weight_tag 15 +#define meshtastic_EnvironmentMetrics_wind_gust_tag 16 +#define meshtastic_EnvironmentMetrics_wind_lull_tag 17 +#define meshtastic_PowerMetrics_ch1_voltage_tag 1 +#define meshtastic_PowerMetrics_ch1_current_tag 2 +#define meshtastic_PowerMetrics_ch2_voltage_tag 3 +#define meshtastic_PowerMetrics_ch2_current_tag 4 +#define meshtastic_PowerMetrics_ch3_voltage_tag 5 +#define meshtastic_PowerMetrics_ch3_current_tag 6 +#define meshtastic_AirQualityMetrics_pm10_standard_tag 1 +#define meshtastic_AirQualityMetrics_pm25_standard_tag 2 +#define meshtastic_AirQualityMetrics_pm100_standard_tag 3 +#define meshtastic_AirQualityMetrics_pm10_environmental_tag 4 +#define meshtastic_AirQualityMetrics_pm25_environmental_tag 5 +#define meshtastic_AirQualityMetrics_pm100_environmental_tag 6 +#define meshtastic_AirQualityMetrics_particles_03um_tag 7 +#define meshtastic_AirQualityMetrics_particles_05um_tag 8 +#define meshtastic_AirQualityMetrics_particles_10um_tag 9 +#define meshtastic_AirQualityMetrics_particles_25um_tag 10 +#define meshtastic_AirQualityMetrics_particles_50um_tag 11 +#define meshtastic_AirQualityMetrics_particles_100um_tag 12 +#define meshtastic_AirQualityMetrics_co2_tag 13 +#define meshtastic_LocalStats_uptime_seconds_tag 1 +#define meshtastic_LocalStats_channel_utilization_tag 2 +#define meshtastic_LocalStats_air_util_tx_tag 3 +#define meshtastic_LocalStats_num_packets_tx_tag 4 +#define meshtastic_LocalStats_num_packets_rx_tag 5 +#define meshtastic_LocalStats_num_packets_rx_bad_tag 6 +#define meshtastic_LocalStats_num_online_nodes_tag 7 +#define meshtastic_LocalStats_num_total_nodes_tag 8 +#define meshtastic_LocalStats_num_rx_dupe_tag 9 +#define meshtastic_LocalStats_num_tx_relay_tag 10 +#define meshtastic_LocalStats_num_tx_relay_canceled_tag 11 +#define meshtastic_HealthMetrics_heart_bpm_tag 1 +#define meshtastic_HealthMetrics_spO2_tag 2 +#define meshtastic_HealthMetrics_temperature_tag 3 +#define meshtastic_Telemetry_time_tag 1 +#define meshtastic_Telemetry_device_metrics_tag 2 +#define meshtastic_Telemetry_environment_metrics_tag 3 +#define meshtastic_Telemetry_air_quality_metrics_tag 4 +#define meshtastic_Telemetry_power_metrics_tag 5 +#define meshtastic_Telemetry_local_stats_tag 6 +#define meshtastic_Telemetry_health_metrics_tag 7 +#define meshtastic_Nau7802Config_zeroOffset_tag 1 +#define meshtastic_Nau7802Config_calibrationFactor_tag 2 + +/* Struct field encoding specification for nanopb */ +#define meshtastic_DeviceMetrics_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, UINT32, battery_level, 1) \ +X(a, STATIC, OPTIONAL, FLOAT, voltage, 2) \ +X(a, STATIC, OPTIONAL, FLOAT, channel_utilization, 3) \ +X(a, STATIC, OPTIONAL, FLOAT, air_util_tx, 4) \ +X(a, STATIC, OPTIONAL, UINT32, uptime_seconds, 5) +#define meshtastic_DeviceMetrics_CALLBACK NULL +#define meshtastic_DeviceMetrics_DEFAULT NULL + +#define meshtastic_EnvironmentMetrics_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, FLOAT, temperature, 1) \ +X(a, STATIC, OPTIONAL, FLOAT, relative_humidity, 2) \ +X(a, STATIC, OPTIONAL, FLOAT, barometric_pressure, 3) \ +X(a, STATIC, OPTIONAL, FLOAT, gas_resistance, 4) \ +X(a, STATIC, OPTIONAL, FLOAT, voltage, 5) \ +X(a, STATIC, OPTIONAL, FLOAT, current, 6) \ +X(a, STATIC, OPTIONAL, UINT32, iaq, 7) \ +X(a, STATIC, OPTIONAL, FLOAT, distance, 8) \ +X(a, STATIC, OPTIONAL, FLOAT, lux, 9) \ +X(a, STATIC, OPTIONAL, FLOAT, white_lux, 10) \ +X(a, STATIC, OPTIONAL, FLOAT, ir_lux, 11) \ +X(a, STATIC, OPTIONAL, FLOAT, uv_lux, 12) \ +X(a, STATIC, OPTIONAL, UINT32, wind_direction, 13) \ +X(a, STATIC, OPTIONAL, FLOAT, wind_speed, 14) \ +X(a, STATIC, OPTIONAL, FLOAT, weight, 15) \ +X(a, STATIC, OPTIONAL, FLOAT, wind_gust, 16) \ +X(a, STATIC, OPTIONAL, FLOAT, wind_lull, 17) +#define meshtastic_EnvironmentMetrics_CALLBACK NULL +#define meshtastic_EnvironmentMetrics_DEFAULT NULL + +#define meshtastic_PowerMetrics_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, FLOAT, ch1_voltage, 1) \ +X(a, STATIC, OPTIONAL, FLOAT, ch1_current, 2) \ +X(a, STATIC, OPTIONAL, FLOAT, ch2_voltage, 3) \ +X(a, STATIC, OPTIONAL, FLOAT, ch2_current, 4) \ +X(a, STATIC, OPTIONAL, FLOAT, ch3_voltage, 5) \ +X(a, STATIC, OPTIONAL, FLOAT, ch3_current, 6) +#define meshtastic_PowerMetrics_CALLBACK NULL +#define meshtastic_PowerMetrics_DEFAULT NULL + +#define meshtastic_AirQualityMetrics_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, UINT32, pm10_standard, 1) \ +X(a, STATIC, OPTIONAL, UINT32, pm25_standard, 2) \ +X(a, STATIC, OPTIONAL, UINT32, pm100_standard, 3) \ +X(a, STATIC, OPTIONAL, UINT32, pm10_environmental, 4) \ +X(a, STATIC, OPTIONAL, UINT32, pm25_environmental, 5) \ +X(a, STATIC, OPTIONAL, UINT32, pm100_environmental, 6) \ +X(a, STATIC, OPTIONAL, UINT32, particles_03um, 7) \ +X(a, STATIC, OPTIONAL, UINT32, particles_05um, 8) \ +X(a, STATIC, OPTIONAL, UINT32, particles_10um, 9) \ +X(a, STATIC, OPTIONAL, UINT32, particles_25um, 10) \ +X(a, STATIC, OPTIONAL, UINT32, particles_50um, 11) \ +X(a, STATIC, OPTIONAL, UINT32, particles_100um, 12) \ +X(a, STATIC, OPTIONAL, UINT32, co2, 13) +#define meshtastic_AirQualityMetrics_CALLBACK NULL +#define meshtastic_AirQualityMetrics_DEFAULT NULL + +#define meshtastic_LocalStats_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, uptime_seconds, 1) \ +X(a, STATIC, SINGULAR, FLOAT, channel_utilization, 2) \ +X(a, STATIC, SINGULAR, FLOAT, air_util_tx, 3) \ +X(a, STATIC, SINGULAR, UINT32, num_packets_tx, 4) \ +X(a, STATIC, SINGULAR, UINT32, num_packets_rx, 5) \ +X(a, STATIC, SINGULAR, UINT32, num_packets_rx_bad, 6) \ +X(a, STATIC, SINGULAR, UINT32, num_online_nodes, 7) \ +X(a, STATIC, SINGULAR, UINT32, num_total_nodes, 8) \ +X(a, STATIC, SINGULAR, UINT32, num_rx_dupe, 9) \ +X(a, STATIC, SINGULAR, UINT32, num_tx_relay, 10) \ +X(a, STATIC, SINGULAR, UINT32, num_tx_relay_canceled, 11) +#define meshtastic_LocalStats_CALLBACK NULL +#define meshtastic_LocalStats_DEFAULT NULL + +#define meshtastic_HealthMetrics_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, UINT32, heart_bpm, 1) \ +X(a, STATIC, OPTIONAL, UINT32, spO2, 2) \ +X(a, STATIC, OPTIONAL, FLOAT, temperature, 3) +#define meshtastic_HealthMetrics_CALLBACK NULL +#define meshtastic_HealthMetrics_DEFAULT NULL + +#define meshtastic_Telemetry_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, FIXED32, time, 1) \ +X(a, STATIC, ONEOF, MESSAGE, (variant,device_metrics,variant.device_metrics), 2) \ +X(a, STATIC, ONEOF, MESSAGE, (variant,environment_metrics,variant.environment_metrics), 3) \ +X(a, STATIC, ONEOF, MESSAGE, (variant,air_quality_metrics,variant.air_quality_metrics), 4) \ +X(a, STATIC, ONEOF, MESSAGE, (variant,power_metrics,variant.power_metrics), 5) \ +X(a, STATIC, ONEOF, MESSAGE, (variant,local_stats,variant.local_stats), 6) \ +X(a, STATIC, ONEOF, MESSAGE, (variant,health_metrics,variant.health_metrics), 7) +#define meshtastic_Telemetry_CALLBACK NULL +#define meshtastic_Telemetry_DEFAULT NULL +#define meshtastic_Telemetry_variant_device_metrics_MSGTYPE meshtastic_DeviceMetrics +#define meshtastic_Telemetry_variant_environment_metrics_MSGTYPE meshtastic_EnvironmentMetrics +#define meshtastic_Telemetry_variant_air_quality_metrics_MSGTYPE meshtastic_AirQualityMetrics +#define meshtastic_Telemetry_variant_power_metrics_MSGTYPE meshtastic_PowerMetrics +#define meshtastic_Telemetry_variant_local_stats_MSGTYPE meshtastic_LocalStats +#define meshtastic_Telemetry_variant_health_metrics_MSGTYPE meshtastic_HealthMetrics + +#define meshtastic_Nau7802Config_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, INT32, zeroOffset, 1) \ +X(a, STATIC, SINGULAR, FLOAT, calibrationFactor, 2) +#define meshtastic_Nau7802Config_CALLBACK NULL +#define meshtastic_Nau7802Config_DEFAULT NULL + +extern const pb_msgdesc_t meshtastic_DeviceMetrics_msg; +extern const pb_msgdesc_t meshtastic_EnvironmentMetrics_msg; +extern const pb_msgdesc_t meshtastic_PowerMetrics_msg; +extern const pb_msgdesc_t meshtastic_AirQualityMetrics_msg; +extern const pb_msgdesc_t meshtastic_LocalStats_msg; +extern const pb_msgdesc_t meshtastic_HealthMetrics_msg; +extern const pb_msgdesc_t meshtastic_Telemetry_msg; +extern const pb_msgdesc_t meshtastic_Nau7802Config_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define meshtastic_DeviceMetrics_fields &meshtastic_DeviceMetrics_msg +#define meshtastic_EnvironmentMetrics_fields &meshtastic_EnvironmentMetrics_msg +#define meshtastic_PowerMetrics_fields &meshtastic_PowerMetrics_msg +#define meshtastic_AirQualityMetrics_fields &meshtastic_AirQualityMetrics_msg +#define meshtastic_LocalStats_fields &meshtastic_LocalStats_msg +#define meshtastic_HealthMetrics_fields &meshtastic_HealthMetrics_msg +#define meshtastic_Telemetry_fields &meshtastic_Telemetry_msg +#define meshtastic_Nau7802Config_fields &meshtastic_Nau7802Config_msg + +/* Maximum encoded size of messages (where known) */ +#define MESHTASTIC_MESHTASTIC_TELEMETRY_PB_H_MAX_SIZE meshtastic_Telemetry_size +#define meshtastic_AirQualityMetrics_size 78 +#define meshtastic_DeviceMetrics_size 27 +#define meshtastic_EnvironmentMetrics_size 85 +#define meshtastic_HealthMetrics_size 11 +#define meshtastic_LocalStats_size 60 +#define meshtastic_Nau7802Config_size 16 +#define meshtastic_PowerMetrics_size 30 +#define meshtastic_Telemetry_size 92 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/mesh/generated/meshtastic/xmodem.pb.cpp b/src/mesh/generated/meshtastic/xmodem.pb.cpp new file mode 100644 index 0000000..3960ccd --- /dev/null +++ b/src/mesh/generated/meshtastic/xmodem.pb.cpp @@ -0,0 +1,14 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9 */ + +#include "meshtastic/xmodem.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(meshtastic_XModem, meshtastic_XModem, AUTO) + + + + + diff --git a/src/mesh/generated/meshtastic/xmodem.pb.h b/src/mesh/generated/meshtastic/xmodem.pb.h new file mode 100644 index 0000000..76edc0d --- /dev/null +++ b/src/mesh/generated/meshtastic/xmodem.pb.h @@ -0,0 +1,78 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9 */ + +#ifndef PB_MESHTASTIC_MESHTASTIC_XMODEM_PB_H_INCLUDED +#define PB_MESHTASTIC_MESHTASTIC_XMODEM_PB_H_INCLUDED +#include + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Enum definitions */ +typedef enum _meshtastic_XModem_Control { + meshtastic_XModem_Control_NUL = 0, + meshtastic_XModem_Control_SOH = 1, + meshtastic_XModem_Control_STX = 2, + meshtastic_XModem_Control_EOT = 4, + meshtastic_XModem_Control_ACK = 6, + meshtastic_XModem_Control_NAK = 21, + meshtastic_XModem_Control_CAN = 24, + meshtastic_XModem_Control_CTRLZ = 26 +} meshtastic_XModem_Control; + +/* Struct definitions */ +typedef PB_BYTES_ARRAY_T(128) meshtastic_XModem_buffer_t; +typedef struct _meshtastic_XModem { + meshtastic_XModem_Control control; + uint16_t seq; + uint16_t crc16; + meshtastic_XModem_buffer_t buffer; +} meshtastic_XModem; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Helper constants for enums */ +#define _meshtastic_XModem_Control_MIN meshtastic_XModem_Control_NUL +#define _meshtastic_XModem_Control_MAX meshtastic_XModem_Control_CTRLZ +#define _meshtastic_XModem_Control_ARRAYSIZE ((meshtastic_XModem_Control)(meshtastic_XModem_Control_CTRLZ+1)) + +#define meshtastic_XModem_control_ENUMTYPE meshtastic_XModem_Control + + +/* Initializer values for message structs */ +#define meshtastic_XModem_init_default {_meshtastic_XModem_Control_MIN, 0, 0, {0, {0}}} +#define meshtastic_XModem_init_zero {_meshtastic_XModem_Control_MIN, 0, 0, {0, {0}}} + +/* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_XModem_control_tag 1 +#define meshtastic_XModem_seq_tag 2 +#define meshtastic_XModem_crc16_tag 3 +#define meshtastic_XModem_buffer_tag 4 + +/* Struct field encoding specification for nanopb */ +#define meshtastic_XModem_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, control, 1) \ +X(a, STATIC, SINGULAR, UINT32, seq, 2) \ +X(a, STATIC, SINGULAR, UINT32, crc16, 3) \ +X(a, STATIC, SINGULAR, BYTES, buffer, 4) +#define meshtastic_XModem_CALLBACK NULL +#define meshtastic_XModem_DEFAULT NULL + +extern const pb_msgdesc_t meshtastic_XModem_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define meshtastic_XModem_fields &meshtastic_XModem_msg + +/* Maximum encoded size of messages (where known) */ +#define MESHTASTIC_MESHTASTIC_XMODEM_PB_H_MAX_SIZE meshtastic_XModem_size +#define meshtastic_XModem_size 141 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp new file mode 100644 index 0000000..8535a33 --- /dev/null +++ b/src/mesh/http/ContentHandler.cpp @@ -0,0 +1,861 @@ +#if !MESHTASTIC_EXCLUDE_WEBSERVER +#include "NodeDB.h" +#include "PowerFSM.h" +#include "RadioLibInterface.h" +#include "airtime.h" +#include "main.h" +#include "mesh/http/ContentHelper.h" +#include "mesh/http/WebServer.h" +#if HAS_WIFI +#include "mesh/wifi/WiFiAPClient.h" +#endif +#include "Led.h" +#include "power.h" +#include "serialization/JSON.h" +#include +#include +#include +#include + +#ifdef ARCH_ESP32 +#include "esp_task_wdt.h" +#endif + +/* + Including the esp32_https_server library will trigger a compile time error. I've + tracked it down to a reoccurrance of this bug: + https://gcc.gnu.org/bugzilla/show_bug.cgi?id=57824 + The work around is described here: + https://forums.xilinx.com/t5/Embedded-Development-Tools/Error-with-Standard-Libaries-in-Zynq/td-p/450032 + + Long story short is we need "#undef str" before including the esp32_https_server. + - Jm Casler (jm@casler.org) Oct 2020 +*/ +#undef str + +// Includes for the https server +// https://github.com/fhessel/esp32_https_server +#include +#include +#include +#include +#include + +// The HTTPS Server comes in a separate namespace. For easier use, include it here. +using namespace httpsserver; + +#include "mesh/http/ContentHandler.h" + +#include +#include +HTTPClient httpClient; + +#define DEST_FS_USES_LITTLEFS + +// We need to specify some content-type mapping, so the resources get delivered with the +// right content type and are displayed correctly in the browser +char contentTypes[][2][32] = {{".txt", "text/plain"}, {".html", "text/html"}, + {".js", "text/javascript"}, {".png", "image/png"}, + {".jpg", "image/jpg"}, {".gz", "application/gzip"}, + {".gif", "image/gif"}, {".json", "application/json"}, + {".css", "text/css"}, {".ico", "image/vnd.microsoft.icon"}, + {".svg", "image/svg+xml"}, {"", ""}}; + +// const char *certificate = NULL; // change this as needed, leave as is for no TLS check (yolo security) + +// Our API to handle messages to and from the radio. +HttpAPI webAPI; + +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 + + ResourceNode *nodeAPIv1ToRadioOptions = new ResourceNode("/api/v1/toradio", "OPTIONS", &handleAPIv1ToRadio); + ResourceNode *nodeAPIv1ToRadio = new ResourceNode("/api/v1/toradio", "PUT", &handleAPIv1ToRadio); + 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 *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 *nodeJsonScanNetworks = new ResourceNode("/json/scanNetworks", "GET", &handleScanNetworks); + ResourceNode *nodeJsonBlinkLED = new ResourceNode("/json/blink", "POST", &handleBlinkLED); + ResourceNode *nodeJsonReport = new ResourceNode("/json/report", "GET", &handleReport); + 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); + + // Secure nodes + secureServer->registerNode(nodeAPIv1ToRadioOptions); + secureServer->registerNode(nodeAPIv1ToRadio); + 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(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(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) +{ + + LOG_DEBUG("webAPI handleAPIv1FromRadio"); + + /* + 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(); + + // 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"); + + uint8_t txBuf[MAX_STREAM_BUF_SIZE]; + uint32_t len = 1; + + 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 { + 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"); + + /* + 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"); + + 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); + } + 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 + thisFileMap["name"] = new JSONValue(String(file.path()).substring(1).c_str()); +#else + thisFileMap["name"] = new JSONValue(String(file.name()).substring(1).c_str()); +#endif + if (String(file.name()).substring(1).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) +{ + res->setHeader("Content-Type", "application/json"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "GET"); + + 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); + + res->print(value->Stringify().c_str()); + + delete value; +} + +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; + if (FSCom.remove(pathDelete.c_str())) { + LOG_INFO("%s", pathDelete.c_str()); + JSONObject jsonObjOuter; + jsonObjOuter["status"] = new JSONValue("ok"); + JSONValue *value = new JSONValue(jsonObjOuter); + res->print(value->Stringify().c_str()); + delete value; + return; + } else { + LOG_INFO("%s", pathDelete.c_str()); + JSONObject jsonObjOuter; + jsonObjOuter["status"] = new JSONValue("Error"); + JSONValue *value = new JSONValue(jsonObjOuter); + res->print(value->Stringify().c_str()); + delete value; + return; + } + } +} + +void handleStatic(HTTPRequest *req, HTTPResponse *res) +{ + // 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"; + } + + 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 - Disabling 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("Skipping unexpected field"); + res->println("

No file found.

"); + return; + } + + // Double check that it is what we expect + if (filename == "") { + LOG_DEBUG("Skipping 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; + + // 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("
");
+    }
+
+    // data->airtime->tx_log
+    JSONArray txLogValues;
+    uint32_t *logArray;
+    logArray = airTime->airtimeReport(TX_LOG);
+    for (int i = 0; i < airTime->getPeriodsToLog(); i++) {
+        txLogValues.push_back(new JSONValue((int)logArray[i]));
+    }
+
+    // data->airtime->rx_log
+    JSONArray rxLogValues;
+    logArray = airTime->airtimeReport(RX_LOG);
+    for (int i = 0; i < airTime->getPeriodsToLog(); i++) {
+        rxLogValues.push_back(new JSONValue((int)logArray[i]));
+    }
+
+    // data->airtime->rx_all_log
+    JSONArray rxAllLogValues;
+    logArray = airTime->airtimeReport(RX_ALL_LOG);
+    for (int i = 0; i < airTime->getPeriodsToLog(); i++) {
+        rxAllLogValues.push_back(new JSONValue((int)logArray[i]));
+    }
+
+    // data->airtime
+    JSONObject jsonObjAirtime;
+    jsonObjAirtime["tx_log"] = new JSONValue(txLogValues);
+    jsonObjAirtime["rx_log"] = new JSONValue(rxLogValues);
+    jsonObjAirtime["rx_all_log"] = new JSONValue(rxAllLogValues);
+    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());
+    jsonObjWifi["ip"] = new JSONValue(WiFi.localIP().toString().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());
+    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()));
+
+    // 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);
+    res->print(value->Stringify().c_str());
+    delete value;
+}
+
+/*
+    This supports the Apple Captive Network Assistant (CNA) Portal
+*/
+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.
+    */
+
+    // 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("");
+}
+
+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("Deleting Content in /static/*"); + + LOG_INFO("Deleting files from /static/* : "); + + htmlDeleteDir("/static"); + + 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"); + + 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"); + + 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. "); + + 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"); + + 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"); + + res->println("

Meshtastic

"); + res->println("Restarting"); + + 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"); + + 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; + } + } else { +#if HAS_SCREEN + screen->blink(); +#endif + } + + JSONObject jsonObjOuter; + jsonObjOuter["status"] = new JSONValue("ok"); + JSONValue *value = new JSONValue(jsonObjOuter); + res->print(value->Stringify().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"); + + 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); + + 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"); + + // serialize and write it to the stream + JSONValue *value = new JSONValue(jsonObjOuter); + res->print(value->Stringify().c_str()); + delete value; +} +#endif \ No newline at end of file diff --git a/src/mesh/http/ContentHandler.h b/src/mesh/http/ContentHandler.h new file mode 100644 index 0000000..987e3ff --- /dev/null +++ b/src/mesh/http/ContentHandler.h @@ -0,0 +1,36 @@ +#pragma once +void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer); + +// Declare some handler functions for the various URLs on the server +void handleAPIv1FromRadio(HTTPRequest *req, HTTPResponse *res); +void handleAPIv1ToRadio(HTTPRequest *req, HTTPResponse *res); +void handleHotspot(HTTPRequest *req, HTTPResponse *res); +void handleStatic(HTTPRequest *req, HTTPResponse *res); +void handleRestart(HTTPRequest *req, HTTPResponse *res); +void handleFormUpload(HTTPRequest *req, HTTPResponse *res); +void handleScanNetworks(HTTPRequest *req, HTTPResponse *res); +void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res); +void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res); +void handleBlinkLED(HTTPRequest *req, HTTPResponse *res); +void handleReport(HTTPRequest *req, HTTPResponse *res); +void handleUpdateFs(HTTPRequest *req, HTTPResponse *res); +void handleDeleteFsContent(HTTPRequest *req, HTTPResponse *res); +void handleFs(HTTPRequest *req, HTTPResponse *res); +void handleAdmin(HTTPRequest *req, HTTPResponse *res); +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 +{ + + public: + // 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 +}; \ No newline at end of file diff --git a/src/mesh/http/ContentHelper.cpp b/src/mesh/http/ContentHelper.cpp new file mode 100644 index 0000000..8f28393 --- /dev/null +++ b/src/mesh/http/ContentHelper.cpp @@ -0,0 +1,14 @@ +#include "mesh/http/ContentHelper.h" +// #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' + } +} diff --git a/src/mesh/http/ContentHelper.h b/src/mesh/http/ContentHelper.h new file mode 100644 index 0000000..a80c39f --- /dev/null +++ b/src/mesh/http/ContentHelper.h @@ -0,0 +1,6 @@ +#include +#include + +#define BoolToString(x) ((x) ? "true" : "false") + +void replaceAll(std::string &str, const std::string &from, const std::string &to); diff --git a/src/mesh/http/WebServer.cpp b/src/mesh/http/WebServer.cpp new file mode 100644 index 0000000..62a8431 --- /dev/null +++ b/src/mesh/http/WebServer.cpp @@ -0,0 +1,213 @@ +#include "configuration.h" +#if !MESHTASTIC_EXCLUDE_WEBSERVER +#include "NodeDB.h" +#include "graphics/Screen.h" +#include "main.h" +#include "mesh/http/WebServer.h" +#include "mesh/wifi/WiFiAPClient.h" +#include "sleep.h" +#include +#include +#include +#include +#include + +#ifdef ARCH_ESP32 +#include "esp_task_wdt.h" +#endif + +// Persistent Data Storage +#include +Preferences prefs; + +/* + Including the esp32_https_server library will trigger a compile time error. I've + tracked it down to a reoccurrance of this bug: + https://gcc.gnu.org/bugzilla/show_bug.cgi?id=57824 + The work around is described here: + https://forums.xilinx.com/t5/Embedded-Development-Tools/Error-with-Standard-Libaries-in-Zynq/td-p/450032 + + Long story short is we need "#undef str" before including the esp32_https_server. + - Jm Casler (jm@casler.org) Oct 2020 +*/ +#undef str + +// Includes for the https server +// https://github.com/fhessel/esp32_https_server +#include +#include +#include +#include +#include + +// The HTTPS Server comes in a separate namespace. For easier use, include it here. +using namespace httpsserver; +#include "mesh/http/ContentHandler.h" + +static SSLCert *cert; +static HTTPSServer *secureServer; +static HTTPServer *insecureServer; + +volatile bool isWebServerReady; +volatile bool isCertReady; + +static void handleWebResponse() +{ + if (isWifiAvailable()) { + + if (isWebServerReady) { + if (secureServer) + secureServer->loop(); + insecureServer->loop(); + } + } +} + +static void taskCreateCert(void *parameter) +{ + prefs.begin("MeshtasticHTTPS", false); + +#if 0 + // Delete the saved certs (used in debugging) + LOG_DEBUG("Deleting any saved SSL keys ..."); + // prefs.clear(); + prefs.remove("PK"); + prefs.remove("cert"); +#endif + + LOG_INFO("Checking if we have a previously saved SSL Certificate."); + + size_t pkLen = prefs.getBytesLength("PK"); + size_t certLen = prefs.getBytesLength("cert"); + + if (pkLen && certLen) { + LOG_INFO("Existing SSL Certificate found!"); + + uint8_t *pkBuffer = new uint8_t[pkLen]; + prefs.getBytes("PK", pkBuffer, pkLen); + + uint8_t *certBuffer = new uint8_t[certLen]; + prefs.getBytes("cert", certBuffer, certLen); + + 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"); + } else { + LOG_INFO("Creating the certificate was successful"); + + 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; + + // Must delete self, can't just fall out + vTaskDelete(NULL); +} + +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. */ + + LOG_DEBUG("Waiting for SSL Cert to be generated."); + while (!isCertReady) { + if ((millis() / 500) % 2) { + if (runLoop) { + LOG_DEBUG("."); + + yield(); + esp_task_wdt_reset(); +#if HAS_SCREEN + if (millis() / 1000 >= 3) { + screen->setSSLFrames(); + } +#endif + } + runLoop = false; + } else { + runLoop = true; + } + } + LOG_INFO("SSL Cert Ready!"); + } +} + +WebServerThread *webServerThread; + +WebServerThread::WebServerThread() : concurrency::OSThread("WebServerThread") +{ + if (!config.network.wifi_enabled) { + disable(); + } +} + +int32_t WebServerThread::runOnce() +{ + if (!config.network.wifi_enabled) { + disable(); + } + + handleWebResponse(); + + if (requestRestart && (millis() / 1000) > requestRestart) { + ESP.restart(); + } + + // Loop every 5ms. + return (5); +} + +void initWebServer() +{ + LOG_DEBUG("Initializing 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("Starting Secure Web Server..."); + secureServer->start(); + } + LOG_INFO("Starting Insecure Web Server..."); + insecureServer->start(); + if (insecureServer->isRunning()) { + LOG_INFO("Web Servers Ready! :-) "); + isWebServerReady = true; + } else { + LOG_ERROR("Web Servers Failed! ;-( "); + } +} +#endif \ No newline at end of file diff --git a/src/mesh/http/WebServer.h b/src/mesh/http/WebServer.h new file mode 100644 index 0000000..815d874 --- /dev/null +++ b/src/mesh/http/WebServer.h @@ -0,0 +1,22 @@ +#pragma once + +#include "PhoneAPI.h" +#include "concurrency/OSThread.h" +#include +#include + +void initWebServer(); +void createSSLCert(); + +class WebServerThread : private concurrency::OSThread +{ + + public: + WebServerThread(); + uint32_t requestRestart = 0; + + protected: + virtual int32_t runOnce() override; +}; + +extern WebServerThread *webServerThread; diff --git a/src/mesh/mesh-pb-constants.cpp b/src/mesh/mesh-pb-constants.cpp new file mode 100644 index 0000000..a279cd9 --- /dev/null +++ b/src/mesh/mesh-pb-constants.cpp @@ -0,0 +1,74 @@ +#include "configuration.h" + +#include "FSCommon.h" +#include "mesh-pb-constants.h" +#include +#include +#include +#include + +/// 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)); + assert( + 0); // If this assert fails it probably means you made a field too large for the max limits specified in mesh.options + 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; + } +} + +#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; + + if (buf == NULL) { + while (count-- && file->read() != EOF) + ; + return count == 0; + } + + status = (file->read(buf, count) == (int)count); + + if (file->available() == 0) + stream->bytes_left = 0; + + return status; +} + +/// Write to an arduino file +bool writecb(pb_ostream_t *stream, const uint8_t *buf, size_t count) +{ + auto file = (Print *)stream->state; + // LOG_DEBUG("writing %d bytes to protobuf file", count); + return file->write(buf, count) == count; +} +#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; + + return false; +} \ No newline at end of file diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h new file mode 100644 index 0000000..f91c485 --- /dev/null +++ b/src/mesh/mesh-pb-constants.h @@ -0,0 +1,50 @@ +#pragma once +#include + +#include "mesh/generated/meshtastic/admin.pb.h" +#include "mesh/generated/meshtastic/deviceonly.pb.h" +#include "mesh/generated/meshtastic/localonly.pb.h" +#include "mesh/generated/meshtastic/mesh.pb.h" + +// this file defines constants which come from mesh.options + +// 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])) +#ifndef MAX_RX_TOPHONE +#define MAX_RX_TOPHONE 32 +#endif + +/// max number of nodes allowed in the mesh +#ifndef MAX_NUM_NODES +#define MAX_NUM_NODES 100 +#endif + +/// Max number of channels allowed +#define MAX_NUM_CHANNELS (member_size(meshtastic_ChannelFile, channels) / member_size(meshtastic_ChannelFile, channels[0])) + +/// 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); + +/// 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); + +/// Read from an Arduino File +bool readcb(pb_istream_t *stream, uint8_t *buf, size_t count); + +/// Write to an arduino file +bool writecb(pb_ostream_t *stream, const uint8_t *buf, size_t count); + +/** is_in_repeated is a macro/function that returns true if a specified word appears in a repeated protobuf array. + * It relies on the following naming conventions from nanopb: + * + * pb_size_t ignore_incoming_count; + * uint32_t ignore_incoming[3]; + */ +bool is_in_helper(uint32_t n, const uint32_t *array, pb_size_t count); + +#define is_in_repeated(name, n) is_in_helper(n, name, name##_count) \ No newline at end of file diff --git a/src/mesh/raspihttp/PiWebServer.cpp b/src/mesh/raspihttp/PiWebServer.cpp new file mode 100644 index 0000000..711a810 --- /dev/null +++ b/src/mesh/raspihttp/PiWebServer.cpp @@ -0,0 +1,530 @@ +/* +Adds a WebServer and WebService callbacks to meshtastic as Linux Version. The WebServer & Webservices +runs in a real linux thread beside the portdunio threading emulation. It replaces the complete ESP32 +Webserver libs including generation of SSL certifcicates, because the use ESP specific details in +the lib that can't be emulated. + +The WebServices adapt to the two major phoneapi functions "handleAPIv1FromRadio,handleAPIv1ToRadio" +The WebServer just adds basaic support to deliver WebContent, so it can be used to +deliver the WebGui definded by the WebClient Project. + +Steps to get it running: +1.) Add these Linux Libs to the compile and target machine: + + sudo apt update && \ + apt -y install openssl libssl-dev libopenssl libsdl2-dev \ + libulfius-dev liborcania-dev + +2.) Configure the root directory of the web Content in the config.yaml file. + The followinng tags should be included and set at your needs + + Example entry in the config.yaml + Webserver: + Port: 9001 # Port for Webserver & Webservices + RootPath: /home/marc/web # Root Dir of WebServer + +3.) Checkout the web project + https://github.com/meshtastic/web.git + + Build it and copy the content of the folder web/dist/* to the folder you did set as "RootPath" + +!!!The WebServer should not be used as production system or exposed to the Internet. Its a raw basic version!!! + +Author: Marc Philipp Hammermann +mail: marchammermann@googlemail.com + +*/ +#ifdef PORTDUINO_LINUX_HARDWARE +#if __has_include() +#include "PiWebServer.h" +#include "NodeDB.h" +#include "PhoneAPI.h" +#include "PowerFSM.h" +#include "RadioLibInterface.h" +#include "airtime.h" +#include "graphics/Screen.h" +#include "main.h" +#include "mesh/wifi/WiFiAPClient.h" +#include "sleep.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "PortduinoFS.h" +#include "platform/portduino/PortduinoGlue.h" + +#define DEFAULT_REALM "default_realm" +#define PREFIX "" + +struct _file_config configWeb; + +// We need to specify some content-type mapping, so the resources get delivered with the +// right content type and are displayed correctly in the browser +char contentTypes[][2][32] = {{".txt", "text/plain"}, {".html", "text/html"}, + {".js", "text/javascript"}, {".png", "image/png"}, + {".jpg", "image/jpg"}, {".gz", "application/gzip"}, + {".gif", "image/gif"}, {".json", "application/json"}, + {".css", "text/css"}, {".ico", "image/vnd.microsoft.icon"}, + {".svg", "image/svg+xml"}, {".ts", "text/javascript"}, + {".tsx", "text/javascript"}, {"", ""}}; + +#undef str + +volatile bool isWebServerReady; +volatile bool isCertReady; + +HttpAPI webAPI; + +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; +} + +/** + * 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; + } +} + +/** + * Cleanup FILE* structure when streaming is complete + */ +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; + + /* + * 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; + } +} + +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 "); + + 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 (req->http_verb == "OPTIONS") { + ulfius_set_response_properties(res, U_OPT_STATUS, 204); + return U_CALLBACK_CONTINUE; + } + + 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); + webAPI.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) +{ + + // 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"); + + uint8_t txBuf[MAX_STREAM_BUF_SIZE]; + uint32_t len = 1; + + if (valueAll == "true") { + while (len) { + len = webAPI.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 = webAPI.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_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); + + // 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; + + 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; + } + + // 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'; + fclose(file); + return buffer; // return pointer +} + +int PiWebServerThread::CheckSSLandLoad() +{ + // read certificate + cert_pem = read_file_into_string("certificate.pem"); + 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("private_key.pem"); + if (key_pem == NULL) { + LOG_ERROR("ERROR file private_key can't be loaded or is missing"); + return 2; + } + + return 0; +} + +int PiWebServerThread::CreateSSLCertificate() +{ + + EVP_PKEY *pkey = NULL; + X509 *x509 = NULL; + + 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 of X509-Certificat."); + return 2; + } + + // Ope file to write private key file + FILE *pkey_file = fopen("private_key.pem", "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("certificate.pem", "wb"); + if (!x509_file) { + LOG_ERROR("Error opening certificate."); + return 4; + } + // write cirtificate + PEM_write_X509(x509_file, x509); + fclose(x509_file); + + EVP_PKEY_free(pkey); + X509_free(x509); + LOG_INFO("Create SSL Certifictate -certificate.pem- succesfull "); + return 0; +} + +void initWebServer() {} + +PiWebServerThread::PiWebServerThread() +{ + int ret, retssl, webservport; + + if (CheckSSLandLoad() != 0) { + CreateSSLCertificate(); + if (CheckSSLandLoad() != 0) { + LOG_ERROR("Major Error Gen & Read SSL Certificate"); + } + } + + if (settingsMap[webserverport] != 0) { + webservport = settingsMap[webserverport]; + LOG_INFO("Using webserver port from yaml config. %i ", webservport); + } else { + LOG_INFO("Webserver port in yaml config set to 0, so defaulting to port 443."); + webservport = 443; + } + + // 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 = settingsStrings[webserverrootpath]; + + 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, NULL); + ulfius_add_endpoint_by_val(&instanceWeb, "PUT", PREFIX, "/api/v1/toradio/*", 1, &handleAPIv1ToRadio, configWeb.rootPath); + + // 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); + + ulfius_stop_framework(&instanceWeb); + ulfius_stop_framework(&instanceWeb); + free(configWeb.rootPath); + ulfius_clean_instance(&instanceService); + ulfius_clean_instance(&instanceService); + free(cert_pem); + LOG_INFO("End framework"); +} + +#endif +#endif \ No newline at end of file diff --git a/src/mesh/raspihttp/PiWebServer.h b/src/mesh/raspihttp/PiWebServer.h new file mode 100644 index 0000000..c4c49e9 --- /dev/null +++ b/src/mesh/raspihttp/PiWebServer.h @@ -0,0 +1,61 @@ +#pragma once +#ifdef PORTDUINO_LINUX_HARDWARE +#if __has_include() +#include "PhoneAPI.h" +#include "ulfius-cfg.h" +#include "ulfius.h" +#include +#include + +#define STATIC_FILE_CHUNK 256 + +void initWebServer(); +void createSSLCert(); +int callback_static_file(const struct _u_request *request, struct _u_response *response, void *user_data); +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; +}; + +class PiWebServerThread +{ + private: + char *key_pem = NULL; + char *cert_pem = NULL; + // struct _u_map mime_types; + std::string webrootpath; + + public: + PiWebServerThread(); + ~PiWebServerThread(); + int CreateSSLCertificate(); + int CheckSSLandLoad(); + uint32_t requestRestart = 0; + struct _u_instance instanceWeb; + struct _u_instance instanceService; +}; + +class HttpAPI : public PhoneAPI +{ + + public: + // 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 +}; + +extern PiWebServerThread *piwebServerThread; + +#endif +#endif \ No newline at end of file diff --git a/src/mesh/wifi/WiFiAPClient.cpp b/src/mesh/wifi/WiFiAPClient.cpp new file mode 100644 index 0000000..e760a82 --- /dev/null +++ b/src/mesh/wifi/WiFiAPClient.cpp @@ -0,0 +1,429 @@ +#include "configuration.h" +#if HAS_WIFI +#include "NodeDB.h" +#include "RTC.h" +#include "concurrency/Periodic.h" +#include "mesh/wifi/WiFiAPClient.h" + +#include "main.h" +#include "mesh/api/WiFiServerAPI.h" +#if !MESHTASTIC_EXCLUDE_MQTT +#include "mqtt/MQTT.h" +#endif +#include "target_specific.h" +#include +#include +#ifdef ARCH_ESP32 +#if !MESHTASTIC_EXCLUDE_WEBSERVER +#include "mesh/http/WebServer.h" +#endif +#include +#include +static void WiFiEvent(WiFiEvent_t event); +#endif + +#ifndef DISABLE_NTP +#include "Throttle.h" +#include +#endif + +using namespace concurrency; + +// NTP +WiFiUDP ntpUDP; + +#ifndef DISABLE_NTP +NTPClient timeClient(ntpUDP, config.network.ntp_server); +#endif + +uint8_t wifiDisconnectReason = 0; + +// Stores our hostname +char ourHost[16]; + +bool APStartupComplete = 0; + +unsigned long lastrun_ntp = 0; + +bool needReconnect = true; // If we create our reconnector, run it once at the beginning +bool isReconnecting = false; // If we are currently reconnecting + +WiFiUDP syslogClient; +Syslog syslog(syslogClient); + +Periodic *wifiReconnect; + +static void onNetworkConnected() +{ + if (!APStartupComplete) { + // Start web server + LOG_INFO("Starting WiFi network services"); + +#ifdef ARCH_ESP32 + // start mdns + if (!MDNS.begin("Meshtastic")) { + LOG_ERROR("Error setting up MDNS responder!"); + } else { + LOG_INFO("mDNS responder started"); + LOG_INFO("mDNS Host: Meshtastic.local"); + MDNS.addService("http", "tcp", 80); + MDNS.addService("https", "tcp", 443); + } +#else // ESP32 handles this in WiFiEvent + LOG_INFO("Obtained IP address: %s", WiFi.localIP().toString().c_str()); +#endif + +#ifndef DISABLE_NTP + LOG_INFO("Starting NTP time client"); + timeClient.begin(); + timeClient.setUpdateInterval(60 * 60); // Update once an hour +#endif + + if (config.network.rsyslog_server[0]) { + LOG_INFO("Starting 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 + initWebServer(); +#endif + initApiServer(); + APStartupComplete = true; + } + + // FIXME this is kinda yucky, instead we should just have an observable for 'wifireconnected' +#ifndef MESHTASTIC_EXCLUDE_MQTT + if (mqtt) + mqtt->reconnect(); +#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); +#else + WiFi.disconnect(false); +#endif + LOG_INFO("Reconnecting to WiFi access point %s", wifiName); + + delay(5000); + + if (!WiFi.isConnected()) { + WiFi.begin(wifiName, wifiPsw); + } + isReconnecting = false; + } + +#ifndef DISABLE_NTP + if (WiFi.isConnected() && (!Throttle::isWithinTimespanMs(lastrun_ntp, 43200000) || (lastrun_ntp == 0))) { // every 12 hours + LOG_DEBUG("Updating 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) + /* If APStartupComplete, but we're not connected, try again. + Shouldn't try again before APStartupComplete. */ + 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; + } else { + return false; + } +} + +// Disable WiFi +void deinitWifi() +{ + LOG_INFO("WiFi deinit"); + + if (isWifiAvailable()) { +#ifdef ARCH_ESP32 + WiFi.disconnect(true, false); +#else + 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 +#endif + esp_wifi_set_storage(WIFI_STORAGE_RAM); // Disable flash storage for WiFi credentials +#endif + 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]); + + WiFi.mode(WIFI_STA); + WiFi.setHostname(ourHost); + + if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_STATIC && + config.network.ipv4_config.ip != 0) { +#ifndef ARCH_RP2040 + WiFi.config(config.network.ipv4_config.ip, config.network.ipv4_config.gateway, config.network.ipv4_config.subnet, + config.network.ipv4_config.dns); +#else + WiFi.config(config.network.ipv4_config.ip, config.network.ipv4_config.dns, config.network.ipv4_config.gateway, + config.network.ipv4_config.subnet); +#endif + } +#ifndef ARCH_RP2040 + 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 + + 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. + + 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; + } +} + +#ifdef ARCH_ESP32 +// Called by the Espressif SDK to +static void WiFiEvent(WiFiEvent_t event) +{ + LOG_DEBUG("WiFi-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"); + break; + case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: + LOG_INFO("Disconnected from WiFi access point"); + 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 IP6 address: %s", WiFi.localIPv6().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_AP_STOP: + LOG_INFO("WiFi access point stopped"); + 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: + LOG_INFO("Ethernet stopped"); + break; + case ARDUINO_EVENT_ETH_CONNECTED: + LOG_INFO("Ethernet connected"); + break; + case ARDUINO_EVENT_ETH_DISCONNECTED: + LOG_INFO("Ethernet disconnected"); + break; + case ARDUINO_EVENT_ETH_GOT_IP: + LOG_INFO("Obtained IP address (ARDUINO_EVENT_ETH_GOT_IP)"); + break; + case ARDUINO_EVENT_ETH_GOT_IP6: + LOG_INFO("Obtained IP6 address (ARDUINO_EVENT_ETH_GOT_IP6)"); + 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("Provisioning: Init"); + break; + case ARDUINO_EVENT_PROV_DEINIT: + LOG_INFO("Provisioning: Stopped"); + break; + case ARDUINO_EVENT_PROV_START: + LOG_INFO("Provisioning: Started"); + break; + case ARDUINO_EVENT_PROV_END: + LOG_INFO("Provisioning: End"); + break; + case ARDUINO_EVENT_PROV_CRED_RECV: + LOG_INFO("Provisioning: Credentials received"); + break; + case ARDUINO_EVENT_PROV_CRED_FAIL: + LOG_INFO("Provisioning: Credentials failed"); + break; + case ARDUINO_EVENT_PROV_CRED_SUCCESS: + LOG_INFO("Provisioning: Credentials success"); + break; + default: + break; + } +} +#endif + +uint8_t getWifiDisconnectReason() +{ + return wifiDisconnectReason; +} +#endif \ No newline at end of file diff --git a/src/mesh/wifi/WiFiAPClient.h b/src/mesh/wifi/WiFiAPClient.h new file mode 100644 index 0000000..5f4e2f5 --- /dev/null +++ b/src/mesh/wifi/WiFiAPClient.h @@ -0,0 +1,22 @@ +#pragma once + +#include "concurrency/Periodic.h" +#include "configuration.h" +#include +#include + +#if HAS_WIFI && !defined(ARCH_PORTDUINO) +#include +#endif + +extern bool needReconnect; +extern concurrency::Periodic *wifiReconnect; + +/// @return true if wifi is now in use +bool initWifi(); + +void deinitWifi(); + +bool isWifiAvailable(); + +uint8_t getWifiDisconnectReason(); \ No newline at end of file diff --git a/src/meshUtils.cpp b/src/meshUtils.cpp new file mode 100644 index 0000000..d211f29 --- /dev/null +++ b/src/meshUtils.cpp @@ -0,0 +1,111 @@ +#include "meshUtils.h" +#include + +/* + * Find the first occurrence of find in s, where the search is limited to the + * first slen characters of s. + * - + * Copyright (c) 2001 Mike Barcroft + * Copyright (c) 1990, 1993 + * The Regents of the University of California. All rights reserved. + * + * This code is derived from software contributed to Berkeley by + * Chris Torek. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + */ +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); + if (labelSize < 100 && numbytes < 64) { + 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 new file mode 100644 index 0000000..47d42b4 --- /dev/null +++ b/src/meshUtils.h @@ -0,0 +1,29 @@ +#pragma once +#include "DebugConfiguration.h" +#include +#include +#include +#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; +} + +#if (defined(ARCH_PORTDUINO) && !defined(STRNSTR)) +#define STRNSTR +#include +char *strnstr(const char *s, const char *find, size_t slen); +#endif + +void printBytes(const char *label, const uint8_t *p, size_t numbytes); + +// is the memory region filled with a single character? +bool memfll(const uint8_t *mem, uint8_t find, size_t numbytes); + +bool isOneOf(int item, int count, ...); + +const std::string vformat(const char *const zcFormat, ...); + +#define IS_ONE_OF(item, ...) isOneOf(item, sizeof((int[]){__VA_ARGS__}) / sizeof(int), __VA_ARGS__) diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp new file mode 100644 index 0000000..90722f5 --- /dev/null +++ b/src/modules/AdminModule.cpp @@ -0,0 +1,1099 @@ +#include "AdminModule.h" +#include "Channels.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "PowerFSM.h" +#include "RTC.h" +#include "meshUtils.h" +#include +#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_BLUETOOTH +#include "BleOta.h" +#endif +#include "Router.h" +#include "configuration.h" +#include "main.h" +#ifdef ARCH_NRF52 +#include "main.h" +#endif +#ifdef ARCH_PORTDUINO +#include "unistd.h" +#endif +#include "../userPrefs.h" +#include "Default.h" +#include "TypeConversions.h" + +#if !MESHTASTIC_EXCLUDE_MQTT +#include "mqtt/MQTT.h" +#endif + +#if !MESHTASTIC_EXCLUDE_GPS +#include "GPS.h" +#endif + +#if MESHTASTIC_EXCLUDE_GPS +#include "modules/PositionModule.h" +#endif + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C +#include "motion/AccelerometerThread.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. +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); + } +} + +/** + * @brief Handle received protobuf message + * + * @param mp Received MeshPacket + * @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("Allowing admin response message"); + } else if (mp.from == 0) { + if (config.security.is_managed) { + LOG_INFO("Ignoring 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("Ignoring admin channel, as 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 { + 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("Ignoring unauthorized admin payload %i", r->which_payload_variant); + myReply = allocErrorResponse(meshtastic_Routing_Error_NOT_AUTHORIZED, &mp); + return handled; + } + + LOG_INFO("Handling 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; + } + } + switch (r->which_payload_variant) { + + /** + * Getters + */ + case meshtastic_AdminMessage_get_owner_request_tag: + LOG_INFO("Client is getting owner"); + handleGetOwner(mp); + break; + + case meshtastic_AdminMessage_get_config_request_tag: + LOG_INFO("Client is getting config"); + handleGetConfig(mp, r->get_config_request); + break; + + case meshtastic_AdminMessage_get_module_config_request_tag: + LOG_INFO("Client is getting 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_INFO("Client is getting 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_INFO("Client is setting owner"); + handleSetOwner(r->set_owner); + break; + + case meshtastic_AdminMessage_set_config_tag: + LOG_INFO("Client is setting the config"); + handleSetConfig(r->set_config); + break; + + case meshtastic_AdminMessage_set_module_config_tag: + LOG_INFO("Client is setting the module config"); + handleSetModuleConfig(r->set_module_config); + break; + + case meshtastic_AdminMessage_set_channel_tag: + LOG_INFO("Client is setting 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_INFO("Client is setting ham mode"); + handleSetHamMode(r->set_ham_mode); + 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) && !MESHTASTIC_EXCLUDE_BLUETOOTH + if (BleOta::getOtaAppVersion().isEmpty()) { + LOG_INFO("No OTA firmware available, scheduling regular reboot in %d seconds", s); + screen->startAlert("Rebooting..."); + } else { + screen->startFirmwareUpdateScreen(); + BleOta::switchToOtaApp(); + LOG_INFO("Rebooting to OTA in %d seconds", s); + } +#else + LOG_INFO("Not on ESP32, scheduling regular reboot in %d seconds", s); + screen->startAlert("Rebooting..."); +#endif + 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 is getting device metadata"); + handleGetDeviceMetadata(mp); + break; + } + case meshtastic_AdminMessage_factory_reset_config_tag: { + disableBluetooth(); + LOG_INFO("Initiating 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("Initiating full factory reset"); + nodeDB->factoryReset(true); + reboot(DEFAULT_REBOOT_SECONDS); + break; + } + case meshtastic_AdminMessage_nodedb_reset_tag: { + disableBluetooth(); + LOG_INFO("Initiating node-db reset"); + nodeDB->resetNodes(); + reboot(DEFAULT_REBOOT_SECONDS); + break; + } + case meshtastic_AdminMessage_begin_edit_settings_tag: { + LOG_INFO("Beginning transaction for editing settings"); + hasOpenEditTransaction = true; + break; + } + case meshtastic_AdminMessage_commit_edit_settings_tag: { + disableBluetooth(); + LOG_INFO("Committing transaction for edited settings"); + hasOpenEditTransaction = false; + saveChanges(SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); + break; + } + case meshtastic_AdminMessage_get_device_connection_status_request_tag: { + LOG_INFO("Client is getting device connection status"); + handleGetDeviceConnectionStatus(mp); + break; + } + case meshtastic_AdminMessage_get_module_config_response_tag: { + LOG_INFO("Client is receiving 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 is receiving a remove_nodenum command."); + nodeDB->removeNodeByNum(r->remove_by_nodenum); + this->notifyObservers(r); // Observed by screen + break; + } + case meshtastic_AdminMessage_set_favorite_node_tag: { + LOG_INFO("Client is receiving a set_favorite_node command."); + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_favorite_node); + if (node != NULL) { + node->is_favorite = true; + saveChanges(SEGMENT_DEVICESTATE, false); + } + break; + } + case meshtastic_AdminMessage_remove_favorite_node_tag: { + LOG_INFO("Client is receiving a remove_favorite_node command."); + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_favorite_node); + if (node != NULL) { + node->is_favorite = false; + saveChanges(SEGMENT_DEVICESTATE, false); + } + break; + } + case meshtastic_AdminMessage_set_fixed_position_tag: { + LOG_INFO("Client is receiving a 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_DEVICESTATE | 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(); +#endif + break; + } + case meshtastic_AdminMessage_remove_fixed_position_tag: { + LOG_INFO("Client is receiving a remove_fixed_position command."); + nodeDB->clearLocalPosition(); + config.position.fixed_position = false; + saveChanges(SEGMENT_DEVICESTATE | SEGMENT_CONFIG, false); + break; + } + case meshtastic_AdminMessage_set_time_only_tag: { + LOG_INFO("Client is receiving a 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 is requesting to enter DFU mode."); +#if defined(ARCH_NRF52) || defined(ARCH_RP2040) + enterDfuMode(); +#endif + break; + } + case meshtastic_AdminMessage_delete_file_request_tag: { + LOG_DEBUG("Client is requesting to delete file: %s", r->delete_file_request); +#ifdef FSCom + if (FSCom.remove(r->delete_file_request)) { + LOG_DEBUG("Successfully deleted file"); + } else { + LOG_DEBUG("Failed to delete file"); + } +#endif + break; + } +#ifdef ARCH_PORTDUINO + 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); + + if (handleResult == AdminMessageHandleResult::HANDLED_WITH_RESPONSE) { + setPassKey(&res); + myReply = allocDataProtobuf(res); + } else if (mp.decoded.want_response) { + LOG_DEBUG("We did not responded to a request that wanted a respond. 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_INFO("Ignoring nonrelevant admin %d", r->which_payload_variant); + } + break; + } + + // 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); + } + + 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. Skipping..."); + 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 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; + + 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)); + } + if (*o.id) { + changed |= strcmp(owner.id, o.id); + strncpy(owner.id, o.id, sizeof(owner.id)); + } + if (owner.is_licensed != o.is_licensed) { + changed = 1; + owner.is_licensed = o.is_licensed; + } + + if (changed) { // If nothing really changed, don't broadcast on the network or write to flash + service->reloadOwner(!hasOpenEditTransaction); + saveChanges(SEGMENT_DEVICESTATE); + } +} + +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("Setting 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(); + } +#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); + } +#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.disable_triple_click == c.payload_variant.device.disable_triple_click && + 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 && + IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_ROUTER, + meshtastic_Config_DeviceConfig_Role_REPEATER)) { + config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_ALL; + const char *warning = "Rebroadcast mode can't be set to NONE for a router or repeater\n"; + 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); + 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 is deprecated; Set it to client + if (c.payload_variant.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_CLIENT) { + 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 Repeater + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) { + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + } +#endif + break; + case meshtastic_Config_position_tag: + LOG_INFO("Setting config: Position"); + config.has_position = true; + config.position = c.payload_variant.position; + // Save nodedb as well in case we got a fixed position packet + saveChanges(SEGMENT_DEVICESTATE, false); + break; + case meshtastic_Config_power_tag: + LOG_INFO("Setting 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; + break; + case meshtastic_Config_network_tag: + LOG_INFO("Setting config: WiFi"); + config.has_network = true; + config.network = c.payload_variant.network; + break; + case meshtastic_Config_display_tag: + LOG_INFO("Setting 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) { + requiresReboot = 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(); + } +#endif + config.display = c.payload_variant.display; + break; + case meshtastic_Config_lora_tag: + LOG_INFO("Setting config: LoRa"); + config.has_lora = true; + // If no lora radio parameters change, don't need to reboot + if (config.lora.use_preset == c.payload_variant.lora.use_preset && config.lora.region == c.payload_variant.lora.region && + config.lora.modem_preset == c.payload_variant.lora.modem_preset && + config.lora.bandwidth == c.payload_variant.lora.bandwidth && + config.lora.spread_factor == c.payload_variant.lora.spread_factor && + config.lora.coding_rate == c.payload_variant.lora.coding_rate && + config.lora.tx_power == c.payload_variant.lora.tx_power && + config.lora.frequency_offset == c.payload_variant.lora.frequency_offset && + config.lora.override_frequency == c.payload_variant.lora.override_frequency && + config.lora.channel_num == c.payload_variant.lora.channel_num && + config.lora.sx126x_rx_boosted_gain == c.payload_variant.lora.sx126x_rx_boosted_gain) { + requiresReboot = false; + } + +#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 = c.payload_variant.lora; + // If we're setting region for the first time, init the region + if (isRegionUnset && config.lora.region > meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + 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 (strcmp(moduleConfig.mqtt.root, default_mqtt_root) == 0) { + sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); + changes = SEGMENT_CONFIG | SEGMENT_MODULECONFIG; + } + } + break; + case meshtastic_Config_bluetooth_tag: + LOG_INFO("Setting config: Bluetooth"); + config.has_bluetooth = true; + config.bluetooth = c.payload_variant.bluetooth; + break; + case meshtastic_Config_security_tag: + LOG_INFO("Setting config: Security"); + config.security = c.payload_variant.security; +#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN) && !(MESHTASTIC_EXCLUDE_PKI) + // We check for a potentially valid private key, and a blank public key, and regen the public key if needed. + if (config.security.private_key.size == 32 && !memfll(config.security.private_key.bytes, 0, 32) && + (config.security.public_key.size == 0 || memfll(config.security.public_key.bytes, 0, 32))) { + 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); +#if !MESHTASTIC_EXCLUDE_PKI + crypto->setDHPrivateKey(config.security.private_key.bytes); +#endif + 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; + } + if (requiresReboot && !hasOpenEditTransaction) { + disableBluetooth(); + } + + saveChanges(changes, requiresReboot); +} + +void AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) +{ + if (!hasOpenEditTransaction) + disableBluetooth(); + switch (c.which_payload_variant) { + case meshtastic_ModuleConfig_mqtt_tag: + LOG_INFO("Setting module config: MQTT"); + moduleConfig.has_mqtt = true; + moduleConfig.mqtt = c.payload_variant.mqtt; + break; + case meshtastic_ModuleConfig_serial_tag: + LOG_INFO("Setting module config: Serial"); + moduleConfig.has_serial = true; + moduleConfig.serial = c.payload_variant.serial; + break; + case meshtastic_ModuleConfig_external_notification_tag: + LOG_INFO("Setting 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("Setting 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("Setting 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("Setting module config: Telemetry"); + moduleConfig.has_telemetry = true; + moduleConfig.telemetry = c.payload_variant.telemetry; + break; + case meshtastic_ModuleConfig_canned_message_tag: + LOG_INFO("Setting 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("Setting module config: Audio"); + moduleConfig.has_audio = true; + moduleConfig.audio = c.payload_variant.audio; + break; + case meshtastic_ModuleConfig_remote_hardware_tag: + LOG_INFO("Setting 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("Setting 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("Setting 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("Setting 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("Setting module config: Paxcounter"); + moduleConfig.has_paxcounter = true; + moduleConfig.paxcounter = c.payload_variant.paxcounter; + break; + } + saveChanges(SEGMENT_MODULECONFIG); +} + +void AdminModule::handleSetChannel(const meshtastic_Channel &cc) +{ + channels.setChannel(cc); + 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); + } +} + +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("Getting 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("Getting 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("Getting 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("Getting 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("Getting 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("Getting 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("Getting 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("Getting 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("Getting config: Sessionkey"); + res.get_config_response.which_payload_variant = meshtastic_Config_sessionkey_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); + } +} + +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("Getting 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("Getting 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("Getting 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("Getting 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("Getting 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("Getting 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("Getting 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("Getting 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("Getting 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("Getting 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("Getting 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("Getting 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("Getting 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); + } +} + +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); +} + +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::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 + 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 !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); +} + +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); + } +} + +void AdminModule::reboot(int32_t seconds) +{ + LOG_INFO("Rebooting in %d seconds", seconds); + screen->startAlert("Rebooting..."); + rebootAtMsec = (seconds < 0) ? 0 : (millis() + seconds * 1000); +} + +void AdminModule::saveChanges(int saveWhat, bool shouldReboot) +{ + if (!hasOpenEditTransaction) { + LOG_INFO("Saving changes to disk"); + service->reloadConfig(saveWhat); // Calls saveToDisk among other things + } else { + LOG_INFO("Delaying save of changes to disk until the open transaction is committed"); + } + if (shouldReboot && !hasOpenEditTransaction) { + reboot(DEFAULT_REBOOT_SECONDS); + } +} + +void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p) +{ + // 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 + auto primaryChannel = channels.getByIndex(channels.getPrimaryIndex()); + auto &channelSettings = primaryChannel.settings; + channelSettings.psk.bytes[0] = 0; + channelSettings.psk.size = 0; + channels.setChannel(primaryChannel); + channels.onConfigChanged(); + + service->reloadOwner(false); + saveChanges(SEGMENT_CONFIG | 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("Setting 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_NodeRemoteHardwarePinsResponse_node_remote_hardware_pins_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) + return true; + else + return false; +} + +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() +{ +#if HAS_BLUETOOTH +#ifdef ARCH_ESP32 + if (nimbleBluetooth) + nimbleBluetooth->deinit(); +#elif defined(ARCH_NRF52) + if (nrf52Bluetooth) + nrf52Bluetooth->shutdown(); +#endif +#endif +} \ No newline at end of file diff --git a/src/modules/AdminModule.h b/src/modules/AdminModule.h new file mode 100644 index 0000000..e54b89a --- /dev/null +++ b/src/modules/AdminModule.h @@ -0,0 +1,65 @@ +#pragma once +#include "ProtobufModule.h" +#if HAS_WIFI +#include "mesh/wifi/WiFiAPClient.h" +#endif + +/** + * Admin module for admin messages + */ +class AdminModule : public ProtobufModule, public Observable +{ + public: + /** Constructor + * name is for debugging output + */ + AdminModule(); + + 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; + + private: + bool hasOpenEditTransaction = false; + + uint8_t session_passkey[8] = {0}; + uint session_time = 0; + + 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); + /** + * Setters + */ + void handleSetOwner(const meshtastic_User &o); + void handleSetChannel(const meshtastic_Channel &cc); + void handleSetConfig(const meshtastic_Config &c); + void handleSetModuleConfig(const meshtastic_ModuleConfig &c); + void handleSetChannel(); + void handleSetHamMode(const meshtastic_HamParameters &req); + void reboot(int32_t seconds); + + 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); +}; + +extern AdminModule *adminModule; + +void disableBluetooth(); \ No newline at end of file diff --git a/src/modules/AtakPluginModule.cpp b/src/modules/AtakPluginModule.cpp new file mode 100644 index 0000000..10c8878 --- /dev/null +++ b/src/modules/AtakPluginModule.cpp @@ -0,0 +1,198 @@ +#include "AtakPluginModule.h" +#include "Default.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "PowerFSM.h" +#include "configuration.h" +#include "main.h" +#include "mesh/compression/unishox2.h" +#include "meshtastic/atak.pb.h" + +AtakPluginModule *atakPluginModule; + +AtakPluginModule::AtakPluginModule() + : ProtobufModule("atak", meshtastic_PortNum_ATAK_PLUGIN, &meshtastic_TAKPacket_msg), concurrency::OSThread("AtakPluginModule") +{ + 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; +} + +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("Compression overflowed contact.callsign. Reverting 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("Compression overflowed contact.device_callsign. Reverting 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("Compression overflowed chat.message. Reverting 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("Compression overflowed chat.to. Reverting 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("Compression overflowed chat.to_callsign. Reverting 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! Skipping"); + return; + } + + // Decompress for Phone (EUD) + auto decompressedCopy = packetPool.allocCopy(mp); + 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("Decompression overflowed 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("Decompression overflowed 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("Decompression overflowed 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("Decompression overflowed 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("Decompression overflowed chat.to_callsign. Bailing out"); + return; + } + LOG_DEBUG("Decompressed chat to_callsign: %d bytes", length); + } + } + 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 new file mode 100644 index 0000000..0470a3b --- /dev/null +++ b/src/modules/AtakPluginModule.h @@ -0,0 +1,26 @@ +#pragma once +#include "ProtobufModule.h" +#include "meshtastic/atak.pb.h" + +/** + * Waypoint message handling for meshtastic + */ +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; + + 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 new file mode 100644 index 0000000..90c8c0f --- /dev/null +++ b/src/modules/CannedMessageModule.cpp @@ -0,0 +1,1259 @@ +#include "configuration.h" +#if ARCH_PORTDUINO +#include "PortduinoGlue.h" +#endif +#if HAS_SCREEN +#include "CannedMessageModule.h" +#include "Channels.h" +#include "FSCommon.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "PowerFSM.h" // needed for button bypass +#include "detect/ScanI2C.h" +#include "input/ScanAndSelect.h" +#include "mesh/generated/meshtastic/cannedmessages.pb.h" +#include "modules/AdminModule.h" + +#include "main.h" // for cardkb_found +#include "modules/ExternalNotificationModule.h" // for buzzer control +#if !MESHTASTIC_EXCLUDE_GPS +#include "GPS.h" +#endif +#if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) +#include "graphics/EInkDynamicDisplay.h" // To select between full and fast refresh on E-Ink displays +#endif + +#ifndef INPUTBROKER_MATRIX_TYPE +#define INPUTBROKER_MATRIX_TYPE 0 +#endif + +#include "graphics/ScreenFonts.h" +#include + +// Remove Canned message screen if no action is taken for some milliseconds +#define INACTIVATE_AFTER_MS 20000 + +extern ScanI2C::DeviceAddress cardkb_found; + +static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto"; + +meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig; + +CannedMessageModule *cannedMessageModule; + +CannedMessageModule::CannedMessageModule() + : SinglePortModule("canned", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("CannedMessageModule") +{ + if (moduleConfig.canned_message.enabled || CANNED_MESSAGE_MODULE_ENABLE) { + 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"); + + // T-Watch interface currently has no way to select destination type, so default to 'node' +#if defined(T_WATCH_S3) || defined(RAK14014) + this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE; +#endif + + this->inputObserver.observe(inputBroker); + } + } else { + this->runState = CANNED_MESSAGE_RUN_STATE_DISABLED; + disable(); + } +} + +/** + * @brief Items in array this->messages will be set to be pointing on the right + * starting points of the string this->messageStore + * + * @return int Returns the number of messages found. + */ +// FIXME: This is just one set of messages now +int CannedMessageModule::splitConfiguredMessages() +{ + int messageIndex = 0; + int i = 0; + + String canned_messages = cannedMessageModuleConfig.messages; + +#if defined(T_WATCH_S3) || defined(RAK14014) + String separator = canned_messages.length() ? "|" : ""; + + canned_messages = "[---- Free Text ----]" + separator + canned_messages; +#endif + + // collect all the message parts + strncpy(this->messageStore, canned_messages.c_str(), sizeof(this->messageStore)); + + // The first message points to the beginning of the store. + this->messages[messageIndex++] = this->messageStore; + int upTo = strlen(this->messageStore) - 1; + + while (i < upTo) { + if (this->messageStore[i] == '|') { + // Message ending found, replace it with string-end character. + this->messageStore[i] = '\0'; + + // hit our max messages, bail + if (messageIndex >= CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT) { + this->messagesCount = messageIndex; + return this->messagesCount; + } + + // Next message starts after pipe (|) just found. + this->messages[messageIndex++] = (this->messageStore + i + 1); + } + i += 1; + } + if (strlen(this->messages[messageIndex - 1]) > 0) { + // We have a last message. + LOG_DEBUG("CannedMessage %d is: '%s'", messageIndex - 1, this->messages[messageIndex - 1]); + this->messagesCount = messageIndex; + } else { + this->messagesCount = messageIndex - 1; + } + + return this->messagesCount; +} + +int CannedMessageModule::handleInputEvent(const InputEvent *event) +{ + if ((strlen(moduleConfig.canned_message.allow_input_source) > 0) && + (strcasecmp(moduleConfig.canned_message.allow_input_source, event->source) != 0) && + (strcasecmp(moduleConfig.canned_message.allow_input_source, "_any") != 0)) { + // Event source is not accepted. + // Event only accepted if source matches the configured one, or + // the configured one is "_any" (or if there is no configured + // source at all) + return 0; + } + if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) { + return 0; // Ignore input while sending + } + bool validEvent = false; + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP)) { + if (this->messagesCount > 0) { + this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP; + validEvent = true; + } + } + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN)) { + if (this->messagesCount > 0) { + this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN; + validEvent = true; + } + } + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) { + +#if defined(T_WATCH_S3) || defined(RAK14014) + if (this->currentMessageIndex == 0) { + this->runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + + requestFocus(); // Tell Screen::setFrames to move to our module's frame, next time it runs + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + this->notifyObservers(&e); + + return 0; + } +#endif + + // when inactive, call the onebutton shortpress instead. Activate Module only on up/down + if ((this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED)) { + powerFSM.trigger(EVENT_PRESS); + } else { + this->payload = this->runState; + this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + validEvent = true; + } + } + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL)) { + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + this->currentMessageIndex = -1; + +#if !defined(T_WATCH_S3) && !defined(RAK14014) + this->freetext = ""; // clear freetext + this->cursor = 0; + this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; +#endif + + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->notifyObservers(&e); + } + if ((event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK)) || + (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) || + (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT))) { + +#if defined(T_WATCH_S3) || defined(RAK14014) + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) { + this->payload = INPUT_BROKER_MSG_LEFT; + } else if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) { + this->payload = INPUT_BROKER_MSG_RIGHT; + } +#else + // tweak for left/right events generated via trackball/touch with empty kbchar + if (!event->kbchar) { + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) { + this->payload = INPUT_BROKER_MSG_LEFT; + } else if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) { + this->payload = INPUT_BROKER_MSG_RIGHT; + } + } else { + // pass the pressed key + this->payload = event->kbchar; + } +#endif + + this->lastTouchMillis = millis(); + validEvent = true; + } + if (event->inputEvent == static_cast(ANYKEY)) { + // when inactive, this will switch to the freetext mode + if ((this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || + (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED)) { + this->runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + } + + validEvent = false; // If key is normal than it will be set to true. + + // Run modifier key code below, (doesnt inturrupt typing or reset to start screen page) + switch (event->kbchar) { + case INPUT_BROKER_MSG_BRIGHTNESS_UP: // make screen brighter + if (screen) + screen->increaseBrightness(); + LOG_DEBUG("increasing Screen Brightness"); + break; + case INPUT_BROKER_MSG_BRIGHTNESS_DOWN: // make screen dimmer + if (screen) + screen->decreaseBrightness(); + LOG_DEBUG("Decreasing Screen Brightness"); + break; + case INPUT_BROKER_MSG_FN_SYMBOL_ON: // draw modifier (function) symbal + if (screen) + screen->setFunctionSymbal("Fn"); + break; + case INPUT_BROKER_MSG_FN_SYMBOL_OFF: // remove modifier (function) symbal + if (screen) + screen->removeFunctionSymbal("Fn"); + break; + // mute (switch off/toggle) external notifications on fn+m + case INPUT_BROKER_MSG_MUTE_TOGGLE: + if (moduleConfig.external_notification.enabled == true) { + if (externalNotificationModule->getMute()) { + externalNotificationModule->setMute(false); + showTemporaryMessage("Notifications \nEnabled"); + if (screen) + screen->removeFunctionSymbal("M"); // remove the mute symbol from the bottom right corner + } else { + externalNotificationModule->stopNow(); // this will turn off all GPIO and sounds and idle the loop + externalNotificationModule->setMute(true); + showTemporaryMessage("Notifications \nDisabled"); + if (screen) + screen->setFunctionSymbal("M"); // add the mute symbol to the bottom right corner + } + } + break; + case INPUT_BROKER_MSG_GPS_TOGGLE: // toggle GPS like triple press does +#if !MESHTASTIC_EXCLUDE_GPS + if (gps != nullptr) { + gps->toggleGpsMode(); + } + if (screen) + screen->forceDisplay(); + showTemporaryMessage("GPS Toggled"); +#endif + break; + case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE: // toggle Bluetooth on/off + if (config.bluetooth.enabled == true) { + config.bluetooth.enabled = false; + LOG_INFO("User toggled Bluetooth"); + nodeDB->saveToDisk(); + disableBluetooth(); + showTemporaryMessage("Bluetooth OFF"); + } else if (config.bluetooth.enabled == false) { + config.bluetooth.enabled = true; + LOG_INFO("User toggled Bluetooth"); + nodeDB->saveToDisk(); + rebootAtMsec = millis() + 2000; + showTemporaryMessage("Bluetooth ON\nReboot"); + } + break; + case INPUT_BROKER_MSG_SEND_PING: // fn+space send network ping like double press does + service->refreshLocalMeshNode(); + if (service->trySendPosition(NODENUM_BROADCAST, true)) { + showTemporaryMessage("Position \nUpdate Sent"); + } else { + showTemporaryMessage("Node Info \nUpdate Sent"); + } + break; + case INPUT_BROKER_MSG_DISMISS_FRAME: // fn+del: dismiss screen frames like text or waypoint + // Avoid opening the canned message screen frame + // We're only handling the keypress here by convention, this has nothing to do with canned messages + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + // Attempt to close whatever frame is currently shown on display + screen->dismissCurrentFrame(); + return 0; + default: + // pass the pressed key + // LOG_DEBUG("Canned message ANYKEY (%x)", event->kbchar); + this->payload = event->kbchar; + this->lastTouchMillis = millis(); + validEvent = true; + break; + } + if (screen && (event->kbchar != INPUT_BROKER_MSG_FN_SYMBOL_ON)) { + screen->removeFunctionSymbal("Fn"); // remove modifier (function) symbal + } + } + +#if defined(T_WATCH_S3) || defined(RAK14014) + if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + String keyTapped = keyForCoordinates(event->touchX, event->touchY); + + if (keyTapped == "⇧") { + this->highlight = -1; + + this->payload = 0x00; + + validEvent = true; + + this->shift = !this->shift; + } else if (keyTapped == "⌫") { + this->highlight = keyTapped[0]; + + this->payload = 0x08; + + validEvent = true; + + this->shift = false; + } else if (keyTapped == "123" || keyTapped == "ABC") { + this->highlight = -1; + + this->payload = 0x00; + + this->charSet = this->charSet == 0 ? 1 : 0; + + validEvent = true; + } else if (keyTapped == " ") { + this->highlight = keyTapped[0]; + + this->payload = keyTapped[0]; + + validEvent = true; + + this->shift = false; + } else if (keyTapped == "↵") { + this->highlight = 0x00; + + this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + + this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; + + this->currentMessageIndex = event->kbchar - 1; + + validEvent = true; + + this->shift = false; + } else if (keyTapped != "") { + this->highlight = keyTapped[0]; + + this->payload = this->shift ? keyTapped[0] : std::tolower(keyTapped[0]); + + validEvent = true; + + this->shift = false; + } + } +#endif + + if (event->inputEvent == static_cast(MATRIXKEY)) { + // this will send the text immediately on matrix press + this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + this->payload = MATRIXKEY; + this->currentMessageIndex = event->kbchar - 1; + this->lastTouchMillis = millis(); + validEvent = true; + } + + if (validEvent) { + requestFocus(); // Tell Screen::setFrames to move to our module's frame, next time it runs + + // Let runOnce to be called immediately. + if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { + setIntervalFromNow(0); // on fast keypresses, this isn't fast enough. + } else { + runOnce(); + } + } + + return 0; +} + +void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies) +{ + meshtastic_MeshPacket *p = allocDataPacket(); + 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); + 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'; // Bell character + p->decoded.payload.size++; + } + + // Only receive routing messages when expecting ACK for a canned message + // Prevents the canned message module from regenerating the screen's frameset at unexpected times, + // or raising a UIFrameEvent before another module has the chance + this->waitingForAck = true; + + LOG_INFO("Sending 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. Even if there's no phone connected, this stores the message to match ACKs +} + +int32_t CannedMessageModule::runOnce() +{ + if (((!moduleConfig.canned_message.enabled) && !CANNED_MESSAGE_MODULE_ENABLE) || + (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { + temporaryMessage = ""; + return INT32_MAX; + } + // LOG_DEBUG("Check status"); + UIFrameEvent e; + if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) || + (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE)) { + // TODO: might have some feedback of sending state + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + temporaryMessage = ""; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + this->currentMessageIndex = -1; + this->freetext = ""; // clear freetext + this->cursor = 0; + +#if !defined(T_WATCH_S3) && !defined(RAK14014) + this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; +#endif + + this->notifyObservers(&e); + } 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 + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + this->currentMessageIndex = -1; + this->freetext = ""; // clear freetext + this->cursor = 0; + +#if !defined(T_WATCH_S3) && !defined(RAK14014) + this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; +#endif + + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->notifyObservers(&e); + } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { + if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + if (this->freetext.length() > 0) { + sendText(this->dest, indexChannels[this->channel], this->freetext.c_str(), true); + this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; + } else { + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + } + } else { + if ((this->messagesCount > this->currentMessageIndex) && (strlen(this->messages[this->currentMessageIndex]) > 0)) { + if (strcmp(this->messages[this->currentMessageIndex], "~") == 0) { + powerFSM.trigger(EVENT_PRESS); + return INT32_MAX; + } else { +#if defined(T_WATCH_S3) || defined(RAK14014) + sendText(this->dest, indexChannels[this->channel], this->messages[this->currentMessageIndex], true); +#else + sendText(NODENUM_BROADCAST, channels.getPrimaryIndex(), this->messages[this->currentMessageIndex], true); +#endif + } + this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; + } else { + // LOG_DEBUG("Reset message is empty."); + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + } + } + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + this->currentMessageIndex = -1; + this->freetext = ""; // clear freetext + this->cursor = 0; + +#if !defined(T_WATCH_S3) && !defined(RAK14014) + this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; +#endif + + this->notifyObservers(&e); + return 2000; + } else if ((this->runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) && (this->currentMessageIndex == -1)) { + this->currentMessageIndex = 0; + LOG_DEBUG("First touch (%d):%s", this->currentMessageIndex, this->getCurrentMessage()); + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_UP) { + if (this->messagesCount > 0) { + this->currentMessageIndex = getPrevIndex(); + this->freetext = ""; // clear freetext + this->cursor = 0; + +#if !defined(T_WATCH_S3) && !defined(RAK14014) + this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; +#endif + + this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + LOG_DEBUG("MOVE UP (%d):%s", this->currentMessageIndex, this->getCurrentMessage()); + } + } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_DOWN) { + if (this->messagesCount > 0) { + this->currentMessageIndex = this->getNextIndex(); + this->freetext = ""; // clear freetext + this->cursor = 0; + +#if !defined(T_WATCH_S3) && !defined(RAK14014) + this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; +#endif + + this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + LOG_DEBUG("MOVE DOWN (%d):%s", this->currentMessageIndex, this->getCurrentMessage()); + } + } else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { + switch (this->payload) { + case INPUT_BROKER_MSG_LEFT: + if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { + size_t numMeshNodes = nodeDB->getNumMeshNodes(); + if (this->dest == NODENUM_BROADCAST) { + this->dest = nodeDB->getNodeNum(); + } + for (unsigned int i = 0; i < numMeshNodes; i++) { + if (nodeDB->getMeshNodeByIndex(i)->num == this->dest) { + this->dest = + (i > 0) ? nodeDB->getMeshNodeByIndex(i - 1)->num : nodeDB->getMeshNodeByIndex(numMeshNodes - 1)->num; + break; + } + } + if (this->dest == nodeDB->getNodeNum()) { + this->dest = NODENUM_BROADCAST; + } + } else if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL) { + for (unsigned int i = 0; i < channels.getNumChannels(); i++) { + if ((channels.getByIndex(i).role == meshtastic_Channel_Role_SECONDARY) || + (channels.getByIndex(i).role == meshtastic_Channel_Role_PRIMARY)) { + indexChannels[numChannels] = i; + numChannels++; + } + } + if (this->channel == 0) { + this->channel = numChannels - 1; + } else { + this->channel--; + } + } else { + if (this->cursor > 0) { + this->cursor--; + } + } + break; + case INPUT_BROKER_MSG_RIGHT: + if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { + size_t numMeshNodes = nodeDB->getNumMeshNodes(); + if (this->dest == NODENUM_BROADCAST) { + this->dest = nodeDB->getNodeNum(); + } + for (unsigned int i = 0; i < numMeshNodes; i++) { + if (nodeDB->getMeshNodeByIndex(i)->num == this->dest) { + this->dest = + (i < numMeshNodes - 1) ? nodeDB->getMeshNodeByIndex(i + 1)->num : nodeDB->getMeshNodeByIndex(0)->num; + break; + } + } + if (this->dest == nodeDB->getNodeNum()) { + this->dest = NODENUM_BROADCAST; + } + } else if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL) { + for (unsigned int i = 0; i < channels.getNumChannels(); i++) { + if ((channels.getByIndex(i).role == meshtastic_Channel_Role_SECONDARY) || + (channels.getByIndex(i).role == meshtastic_Channel_Role_PRIMARY)) { + indexChannels[numChannels] = i; + numChannels++; + } + } + if (this->channel == numChannels - 1) { + this->channel = 0; + } else { + this->channel++; + } + } else { + if (this->cursor < this->freetext.length()) { + this->cursor++; + } + } + break; + default: + break; + } + if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + switch (this->payload) { // code below all trigger the freetext window (where you type to send a message) or reset the + // display back to the default window + case 0x08: // backspace + if (this->freetext.length() > 0 && this->highlight == 0x00) { + 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--; + } + break; + case 0x09: // tab + if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL) { + this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; + } else if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { + this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL; + } else { + this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE; + } + break; + case INPUT_BROKER_MSG_LEFT: + case INPUT_BROKER_MSG_RIGHT: + // already handled above + break; + // handle fn+s for shutdown + case INPUT_BROKER_MSG_SHUTDOWN: + if (screen) + screen->startAlert("Shutting down..."); + shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000; + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + break; + // and fn+r for reboot + case INPUT_BROKER_MSG_REBOOT: + if (screen) + screen->startAlert("Rebooting..."); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + break; + default: + if (this->highlight != 0x00) { + break; + } + + if (this->cursor == this->freetext.length()) { + this->freetext += this->payload; + } else { + this->freetext = + this->freetext.substring(0, this->cursor) + this->payload + this->freetext.substring(this->cursor); + } + + this->cursor += 1; + + uint16_t maxChars = meshtastic_Constants_DATA_PAYLOAD_LEN - (moduleConfig.canned_message.send_bell ? 1 : 0); + if (this->freetext.length() > maxChars) { + this->cursor = maxChars; + this->freetext = this->freetext.substring(0, maxChars); + } + break; + } + if (screen) + screen->removeFunctionSymbal("Fn"); + } + + 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; +} + +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"; + } else { + meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); + if (info != NULL) { + return info->user.long_name; + } else { + return "Unknown"; + } + } +} + +bool CannedMessageModule::shouldDraw() +{ + if (!moduleConfig.canned_message.enabled && !CANNED_MESSAGE_MODULE_ENABLE) { + return false; + } + + // If using "scan and select" input, don't draw the module frame just to say "disabled" + // The scanAndSelectInput class will draw its own temporary alert for user, when the input button is pressed + else if (scanAndSelectInput != nullptr && !hasMessages()) + return false; + + return (currentMessageIndex != -1) || (this->runState != CANNED_MESSAGE_RUN_STATE_INACTIVE); +} + +// 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; + } +} + +int CannedMessageModule::getPrevIndex() +{ + if (this->currentMessageIndex <= 0) { + return this->messagesCount - 1; + } else { + return this->currentMessageIndex - 1; + } +} +void CannedMessageModule::showTemporaryMessage(const String &message) +{ + temporaryMessage = message; + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + notifyObservers(&e); + runState = CANNED_MESSAGE_RUN_STATE_MESSAGE; + // run this loop again in 2 seconds, next iteration will clear the display + setIntervalFromNow(2000); +} + +#if defined(T_WATCH_S3) || defined(RAK14014) + +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 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; + } + } + } + + 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]; + + int xOffset = 0; + + int yOffset = 56; + + display->setTextAlignment(TEXT_ALIGN_LEFT); + + display->setFont(FONT_SMALL); + + display->setColor(OLEDDISPLAY_COLOR::WHITE); + + display->drawStringMaxWidth(0, 0, display->getWidth(), + cannedMessageModule->drawWithCursor(cannedMessageModule->freetext, cannedMessageModule->cursor)); + + display->setFont(FONT_MEDIUM); + + int cellHeight = round((display->height() - 64) / outerSize); + + int yCorrection = 8; + + 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 innerSize = 0; + + for (int8_t innerIndex = 0; innerIndex < innerSizeBound; innerIndex++) { + if (this->keyboard[this->charSet][outerIndex][innerIndex].character != "") { + innerSize++; + } + } + + int cellWidth = display->width() / innerSize; + + for (int8_t innerIndex = 0; innerIndex < innerSize; innerIndex++) { + xOffset += innerIndex > 0 ? cellWidth : 0; + + Letter letter = this->keyboard[this->charSet][outerIndex][innerIndex]; + + Letter updatedLetter = {letter.character, letter.width, xOffset, yOffset, cellWidth, cellHeight}; + + this->keyboard[this->charSet][outerIndex][innerIndex] = updatedLetter; + + float characterOffset = ((cellWidth / 2) - (letter.width / 2)); + + if (letter.character == "⇧") { + if (this->shift) { + display->fillRect(xOffset, yOffset, cellWidth, cellHeight); + + display->setColor(OLEDDISPLAY_COLOR::BLACK); + + drawShiftIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2); + + 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); + } + } + } + + 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}}; + + 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); + + 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}}; + + 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); + + display->drawLine(x0, y0, x1, y1); + } + + PointStruct backspaceIconX[4] = {{7, 4}, {13, 10}, {7, 10}, {13, 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); + + 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}}; + + 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); + + 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; + } +} + +void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + char buffer[50]; + + if (temporaryMessage.length() != 0) { + requestFocus(); // Tell Screen::setFrames to move to our module's frame + LOG_DEBUG("Drawing temporary message: %s", temporaryMessage.c_str()); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(display->getWidth() / 2 + x, 0 + y + 12, temporaryMessage); + } else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) { + requestFocus(); // Tell Screen::setFrames to move to our module's frame + EINK_ADD_FRAMEFLAG(display, COSMETIC); // Clean after this popup. Layout makes ghosting particularly obvious + +#ifdef USE_EINK + display->setFont(FONT_SMALL); // No chunky text +#else + display->setFont(FONT_MEDIUM); // Chunky text +#endif + + String displayString; + display->setTextAlignment(TEXT_ALIGN_CENTER); + if (this->ack) { + displayString = "Delivered to\n%s"; + } else { + displayString = "Delivery failed\nto %s"; + } + display->drawStringf(display->getWidth() / 2 + x, 0 + y + 12, buffer, displayString, + cannedMessageModule->getNodeName(this->incoming)); + + display->setFont(FONT_SMALL); + + String snrString = "Last Rx SNR: %f"; + String rssiString = "Last Rx RSSI: %d"; + + // Don't bother drawing snr and rssi for tiny displays + if (display->getHeight() > 100) { + + // Original implementation used constants of y = 100 and y = 130. Shrink this if screen is *slightly* small + int16_t snrY = 100; + int16_t rssiY = 130; + + // If dislay is *slighly* too small for the original consants, squish up a bit + if (display->getHeight() < rssiY + FONT_HEIGHT_SMALL) { + snrY = display->getHeight() - ((1.5) * FONT_HEIGHT_SMALL); + rssiY = display->getHeight() - ((2.5) * FONT_HEIGHT_SMALL); + } + + if (this->ack) { + display->drawStringf(display->getWidth() / 2 + x, snrY + y, buffer, snrString, this->lastRxSnr); + display->drawStringf(display->getWidth() / 2 + x, rssiY + y, buffer, rssiString, this->lastRxRssi); + } + } + } else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) { + // E-Ink: clean the screen *after* this pop-up + EINK_ADD_FRAMEFLAG(display, COSMETIC); + + requestFocus(); // Tell Screen::setFrames to move to our module's frame + +#ifdef USE_EINK + display->setFont(FONT_SMALL); // No chunky text +#else + display->setFont(FONT_MEDIUM); // Chunky text +#endif + + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(display->getWidth() / 2 + x, 0 + y + 12, "Sending..."); + } else if (cannedMessageModule->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."); + } else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + requestFocus(); // Tell Screen::setFrames to move to our module's frame +#if defined(T_WATCH_S3) || defined(RAK14014) + drawKeyboard(display, state, 0, 0); +#else + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + if (this->destSelect != CANNED_MESSAGE_DESTINATION_TYPE_NONE) { + display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); + display->setColor(BLACK); + } + switch (this->destSelect) { + case CANNED_MESSAGE_DESTINATION_TYPE_NODE: + display->drawStringf(1 + x, 0 + y, buffer, "To: >%s<@%s", cannedMessageModule->getNodeName(this->dest), + channels.getName(indexChannels[this->channel])); + display->drawStringf(0 + x, 0 + y, buffer, "To: >%s<@%s", cannedMessageModule->getNodeName(this->dest), + channels.getName(indexChannels[this->channel])); + break; + case CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL: + display->drawStringf(1 + x, 0 + y, buffer, "To: %s@>%s<", cannedMessageModule->getNodeName(this->dest), + channels.getName(indexChannels[this->channel])); + display->drawStringf(0 + x, 0 + y, buffer, "To: %s@>%s<", cannedMessageModule->getNodeName(this->dest), + channels.getName(indexChannels[this->channel])); + break; + default: + if (display->getWidth() > 128) { + display->drawStringf(0 + x, 0 + y, buffer, "To: %s@%s", cannedMessageModule->getNodeName(this->dest), + channels.getName(indexChannels[this->channel])); + } else { + display->drawStringf(0 + x, 0 + y, buffer, "To: %.5s@%.5s", cannedMessageModule->getNodeName(this->dest), + channels.getName(indexChannels[this->channel])); + } + break; + } + // used chars right aligned, only when not editing the destination + if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) { + 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); + } + display->setColor(WHITE); + display->drawStringMaxWidth( + 0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(), + cannedMessageModule->drawWithCursor(cannedMessageModule->freetext, cannedMessageModule->cursor)); +#endif + } else { + if (this->messagesCount > 0) { + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawStringf(0 + x, 0 + y, buffer, "To: %s", cannedMessageModule->getNodeName(this->dest)); + int lines = (display->getHeight() / FONT_HEIGHT_SMALL) - 1; + if (lines == 3) { + // static (old) behavior for small displays + display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL, cannedMessageModule->getPrevMessage()); + display->fillRect(0 + x, 0 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(), y + FONT_HEIGHT_SMALL); + display->setColor(BLACK); + display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * 2, cannedMessageModule->getCurrentMessage()); + display->setColor(WHITE); + display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * 3, cannedMessageModule->getNextMessage()); + } else { + // use entire display height for larger displays + int topMsg = (messagesCount > lines && currentMessageIndex >= lines - 1) ? currentMessageIndex - lines + 2 : 0; + for (int i = 0; i < std::min(messagesCount, lines); i++) { + if (i == currentMessageIndex - topMsg) { +#ifdef USE_EINK + // Avoid drawing solid black with fillRect: harder to clear for E-Ink + display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), ">"); + display->drawString(12 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), + cannedMessageModule->getCurrentMessage()); +#else + display->fillRect(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), x + display->getWidth(), + y + FONT_HEIGHT_SMALL); + display->setColor(BLACK); + display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), cannedMessageModule->getCurrentMessage()); + display->setColor(WHITE); +#endif + } else { + display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), + cannedMessageModule->getMessageByIndex(topMsg + i)); + } + } + } + } + } +} + +ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket &mp) +{ + if (mp.decoded.portnum == meshtastic_PortNum_ROUTING_APP && waitingForAck) { + // look for a request_id + if (mp.decoded.request_id != 0) { + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + requestFocus(); // Tell Screen::setFrames that our module's frame should be shown, even if not "first" in the frameset + this->runState = CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED; + this->incoming = service->getNodenumFromRequestId(mp.decoded.request_id); + meshtastic_Routing decoded = meshtastic_Routing_init_default; + pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_Routing_fields, &decoded); + this->ack = decoded.error_reason == meshtastic_Routing_Error_NONE; + waitingForAck = false; // No longer want routing packets + this->notifyObservers(&e); + // run the next time 2 seconds later + setIntervalFromNow(2000); + } + } + + 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. + * + * @return true On success. + * @return false On error. + */ +bool CannedMessageModule::saveProtoForModule() +{ + bool okay = true; + +#ifdef FS + FS.mkdir("/prefs"); +#endif + + okay &= nodeDB->saveProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size, + &meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig); + + return okay; +} + +/** + * @brief Fill configuration with default values. + */ +void CannedMessageModule::installDefaultCannedMessageModuleConfig() +{ + memset(cannedMessageModuleConfig.messages, 0, sizeof(cannedMessageModuleConfig.messages)); +} + +/** + * @brief An admin message arrived to AdminModule. We are asked whether we want to handle that. + * + * @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_RESULT if a result is also prepared. + */ +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 is 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 is setting 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(); + } +} + +String CannedMessageModule::drawWithCursor(String text, int cursor) +{ + String result = text.substring(0, cursor) + "_" + text.substring(cursor); + return result; +} + +#endif \ No newline at end of file diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h new file mode 100644 index 0000000..e9dc2bd --- /dev/null +++ b/src/modules/CannedMessageModule.h @@ -0,0 +1,227 @@ +#pragma once +#if HAS_SCREEN +#include "ProtobufModule.h" +#include "input/InputBroker.h" + +enum cannedMessageModuleRunState { + CANNED_MESSAGE_RUN_STATE_DISABLED, + CANNED_MESSAGE_RUN_STATE_INACTIVE, + CANNED_MESSAGE_RUN_STATE_ACTIVE, + CANNED_MESSAGE_RUN_STATE_FREETEXT, + CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE, + CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED, + CANNED_MESSAGE_RUN_STATE_MESSAGE, + CANNED_MESSAGE_RUN_STATE_ACTION_SELECT, + CANNED_MESSAGE_RUN_STATE_ACTION_UP, + CANNED_MESSAGE_RUN_STATE_ACTION_DOWN, +}; + +enum cannedMessageDestinationType { + CANNED_MESSAGE_DESTINATION_TYPE_NONE, + CANNED_MESSAGE_DESTINATION_TYPE_NODE, + CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL +}; + +enum CannedMessageModuleIconType { shift, backspace, space, enter }; + +struct Letter { + String character; + float width; + int rectX; + int rectY; + int rectWidth; + int rectHeight; +}; + +#define CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT 50 +/** + * Sum of CannedMessageModuleConfig part sizes. + */ +#define CANNED_MESSAGE_MODULE_MESSAGES_SIZE 800 + +#ifndef CANNED_MESSAGE_MODULE_ENABLE +#define CANNED_MESSAGE_MODULE_ENABLE 0 +#endif + +class CannedMessageModule : public SinglePortModule, public Observable, private concurrency::OSThread +{ + CallbackObserver inputObserver = + CallbackObserver(this, &CannedMessageModule::handleInputEvent); + + public: + CannedMessageModule(); + const char *getCurrentMessage(); + const char *getPrevMessage(); + const char *getNextMessage(); + const char *getMessageByIndex(int index); + const char *getNodeName(NodeNum node); + bool shouldDraw(); + bool hasMessages(); + // void eventUp(); + // void eventDown(); + // void eventSelect(); + + void handleGetCannedMessageModuleMessages(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response); + void handleSetCannedMessageModuleMessages(const char *from_msg); + + void showTemporaryMessage(const String &message); + + String drawWithCursor(String text, int cursor); + + /* + -Override the wantPacket method. We need the Routing Messages to look for ACKs. + */ + virtual bool wantPacket(const meshtastic_MeshPacket *p) override + { + if (p->rx_rssi != 0) { + this->lastRxRssi = p->rx_rssi; + } + + if (p->rx_snr > 0) { + this->lastRxSnr = p->rx_snr; + } + + switch (p->decoded.portnum) { + case meshtastic_PortNum_ROUTING_APP: + return waitingForAck; + default: + return false; + } + } + + protected: + virtual int32_t runOnce() override; + + void sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies); + + int splitConfiguredMessages(); + int getNextIndex(); + int getPrevIndex(); + +#if defined(T_WATCH_S3) || defined(RAK14014) + void drawKeyboard(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + String keyForCoordinates(uint x, uint y); + bool shift = false; + int charSet = 0; + 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 + + char highlight = 0x00; + + int handleInputEvent(const InputEvent *event); + virtual bool wantUIFrame() override { return this->shouldDraw(); } + virtual Observable *getUIFrameObservable() override { return this; } + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; + virtual bool interceptingKeyboardInput() override; + virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) 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; + + void loadProtoForModule(); + bool saveProtoForModule(); + + void installDefaultCannedMessageModuleConfig(); + + int currentMessageIndex = -1; + cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + char payload = 0x00; + unsigned int cursor = 0; + String freetext = ""; // Text Buffer for Freetext Editor + NodeNum dest = NODENUM_BROADCAST; + ChannelIndex channel = 0; + cannedMessageDestinationType destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; + uint8_t numChannels = 0; + ChannelIndex indexChannels[MAX_NUM_CHANNELS] = {0}; + NodeNum incoming = NODENUM_BROADCAST; + bool ack = false; // True means ACK, false means NAK (error_reason != NONE) + bool waitingForAck = false; // Are currently interested in routing packets? + float lastRxSnr = 0; + int32_t lastRxRssi = 0; + + char messageStore[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1]; + char *messages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT]; + int messagesCount = 0; + unsigned long lastTouchMillis = 0; + String temporaryMessage; + +#if defined(T_WATCH_S3) || defined(RAK14014) + 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 +}; + +extern CannedMessageModule *cannedMessageModule; +#endif \ No newline at end of file diff --git a/src/modules/DetectionSensorModule.cpp b/src/modules/DetectionSensorModule.cpp new file mode 100644 index 0000000..99f9664 --- /dev/null +++ b/src/modules/DetectionSensorModule.cpp @@ -0,0 +1,159 @@ +#include "DetectionSensorModule.h" +#include "Default.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "PowerFSM.h" +#include "configuration.h" +#include "main.h" +#include +DetectionSensorModule *detectionSensorModule; + +#define GPIO_POLLING_INTERVAL 100 +#define DELAYED_INTERVAL 1000 + +typedef enum { + 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_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; +} + +const static DetectionSensorTriggerHandler handlers[_meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_MAX + 1] = { + [meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_LOW] = detection_trigger_logic_level, + [meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_HIGH] = detection_trigger_logic_level, + [meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_FALLING_EDGE] = detection_trigger_single_edge, + [meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_RISING_EDGE] = detection_trigger_single_edge, + [meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_EITHER_EDGE_ACTIVE_LOW] = detection_trigger_either_edge, + [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"); + + if (moduleConfig.detection_sensor.enabled == false) + return disable(); + + if (firstTime) { + +#ifdef DETECTION_SENSOR_EN + 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. Disabling module..."); + return disable(); + } + LOG_INFO("Detection Sensor Module: Initializing"); + + return DELAYED_INTERVAL; + } + + // 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; +} + +void DetectionSensorModule::sendDetectionMessage() +{ + LOG_DEBUG("Detected event observed. Sending 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++; + } + LOG_INFO("Sending message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); + lastSentToMesh = millis(); + service->sendToMesh(p); + 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); + LOG_INFO("Sending message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); + lastSentToMesh = millis(); + service->sendToMesh(p); + 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; +} \ No newline at end of file diff --git a/src/modules/DetectionSensorModule.h b/src/modules/DetectionSensorModule.h new file mode 100644 index 0000000..b960c87 --- /dev/null +++ b/src/modules/DetectionSensorModule.h @@ -0,0 +1,24 @@ +#pragma once +#include "SinglePortModule.h" + +class DetectionSensorModule : public SinglePortModule, private concurrency::OSThread +{ + public: + DetectionSensorModule() + : SinglePortModule("detection", meshtastic_PortNum_DETECTION_SENSOR_APP), OSThread("DetectionSensorModule") + { + } + + 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(); +}; + +extern DetectionSensorModule *detectionSensorModule; \ No newline at end of file diff --git a/src/modules/DropzoneModule.cpp b/src/modules/DropzoneModule.cpp new file mode 100644 index 0000000..6c42af9 --- /dev/null +++ b/src/modules/DropzoneModule.cpp @@ -0,0 +1,95 @@ +#if !MESHTASTIC_EXCLUDE_DROPZONE + +#include "DropzoneModule.h" +#include "Meshservice->h" +#include "configuration.h" +#include "gps/GeoCoord.h" +#include "gps/RTC.h" +#include "main.h" + +#include + +#include "modules/Telemetry/Sensor/DFRobotLarkSensor.h" +#include "modules/Telemetry/UnitConversions.h" + +#include + +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; +} + +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; +} + +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; + + hour = hms / SEC_PER_HOUR; + min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; + } + + // 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); + + return reply; +} + +#endif \ No newline at end of file diff --git a/src/modules/DropzoneModule.h b/src/modules/DropzoneModule.h new file mode 100644 index 0000000..28f54ee --- /dev/null +++ b/src/modules/DropzoneModule.h @@ -0,0 +1,37 @@ +#pragma once +#if !MESHTASTIC_EXCLUDE_DROPZONE +#include "SinglePortModule.h" +#include "modules/Telemetry/Sensor/DFRobotLarkSensor.h" + +/** + * 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; + + public: + /** Constructor + * name is for debugging output + */ + DropzoneModule() : SinglePortModule("dropzone", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("DropzoneModule") + { + // Set up the analog pin for reading the dropzone status + pinMode(PIN_A1, INPUT); + } + + virtual int32_t runOnce() 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; +}; + +extern DropzoneModule *dropzoneModule; +#endif \ No newline at end of file diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp new file mode 100644 index 0000000..87387f0 --- /dev/null +++ b/src/modules/ExternalNotificationModule.cpp @@ -0,0 +1,603 @@ +/** + * @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. + * + * Documentation: + * https://meshtastic.org/docs/configuration/module/external-notification + * + * @author Jm Casler & Meshtastic Team + * @date [Insert Date] + */ +#include "ExternalNotificationModule.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "RTC.h" +#include "Router.h" +#include "buzz/buzz.h" +#include "configuration.h" +#include "main.h" +#include "mesh/generated/meshtastic/rtttl.pb.h" +#include + +#ifdef HAS_NCP5623 +#include +#endif + +#ifdef HAS_NEOPIXEL +#include +#endif + +#ifdef UNPHONE +#include "unPhone.h" +extern unPhone unphone; +#endif + +#if defined(HAS_NCP5623) || defined(RGBLED_RED) || defined(HAS_NEOPIXEL) || defined(UNPHONE) +uint8_t red = 0; +uint8_t green = 0; +uint8_t blue = 0; +uint8_t colorState = 1; +uint8_t brightnessIndex = 0; +uint8_t brightnessValues[] = {0, 10, 20, 30, 50, 90, 160, 170}; // blue gets multiplied by 1.5 +bool ascending = true; +#endif + +#ifndef PIN_BUZZER +#define PIN_BUZZER false +#endif + +/* + Documentation: + https://meshtastic.org/docs/configuration/module/external-notification +*/ + +// Default configurations +#ifdef EXT_NOTIFY_OUT +#define EXT_NOTIFICATION_MODULE_OUTPUT EXT_NOTIFY_OUT +#else +#define EXT_NOTIFICATION_MODULE_OUTPUT 0 +#endif +#define EXT_NOTIFICATION_MODULE_OUTPUT_MS 1000 + +#define EXT_NOTIFICATION_DEFAULT_THREAD_MS 25 + +#define ASCII_BELL 0x07 + +meshtastic_RTTTLConfig rtttlConfig; + +ExternalNotificationModule *externalNotificationModule; + +bool externalCurrentState[3] = {}; + +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 { + + bool isPlaying = rtttl::isPlaying(); +#ifdef HAS_I2S + isPlaying = rtttl::isPlaying() || audioThread->isPlaying(); +#endif + if ((nagCycleCutoff < millis()) && !isPlaying) { + // let the song finish if we reach timeout + nagCycleCutoff = UINT32_MAX; + LOG_INFO("Turning off external notification: "); + for (int i = 0; i < 3; i++) { + setExternalOff(i); + externalTurnedOn[i] = 0; + LOG_INFO("%d ", i); + } + LOG_INFO(""); +#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 +#if defined(T_DECK) || (defined(BUTTON_PIN) && BUTTON_PIN == 0) + pinMode(0, INPUT); +#endif +#endif + 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) { + if (externalTurnedOn[0] + (moduleConfig.external_notification.output_ms ? moduleConfig.external_notification.output_ms + : EXT_NOTIFICATION_MODULE_OUTPUT_MS) < + millis()) { + getExternal(0) ? setExternalOff(0) : setExternalOn(0); + } + if (externalTurnedOn[1] + (moduleConfig.external_notification.output_ms ? moduleConfig.external_notification.output_ms + : EXT_NOTIFICATION_MODULE_OUTPUT_MS) < + millis()) { + getExternal(1) ? setExternalOff(1) : setExternalOn(1); + } + if (externalTurnedOn[2] + (moduleConfig.external_notification.output_ms ? moduleConfig.external_notification.output_ms + : EXT_NOTIFICATION_MODULE_OUTPUT_MS) < + millis()) { + getExternal(2) ? setExternalOff(2) : setExternalOn(2); + } +#if defined(HAS_NCP5623) || defined(RGBLED_RED) || defined(HAS_NEOPIXEL) || defined(UNPHONE) + 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 +#ifdef HAS_NCP5623 + if (rgb_found.type == ScanI2C::NCP5623) { + rgb.setColor(red, green, blue); + } +#endif +#ifdef RGBLED_CA + 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); +#endif +#ifdef HAS_NEOPIXEL + pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT); + pixels.show(); +#endif +#ifdef UNPHONE + 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; + } + } +#endif + +#ifdef T_WATCH_S3 + 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)); + } + } +#endif + // now let the PWM buzzer play + if (moduleConfig.external_notification.use_pwm && config.device.buzzer_gpio) { + 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); + } + } + + return EXT_NOTIFICATION_DEFAULT_THREAD_MS; + } +} + +bool ExternalNotificationModule::wantPacket(const meshtastic_MeshPacket *p) +{ + return MeshService::isTextPayload(p); +} + +/** + * Sets the external notification on for the specified index. + * + * @param index The index of the external notification to turn on. + */ +void ExternalNotificationModule::setExternalOn(uint8_t index) +{ + externalCurrentState[index] = 1; + externalTurnedOn[index] = millis(); + + switch (index) { + case 1: +#ifdef UNPHONE + unphone.vibe(true); // the unPhone's vibration motor is on a i2c GPIO expander +#endif + if (moduleConfig.external_notification.output_vibra) + digitalWrite(moduleConfig.external_notification.output_vibra, true); + break; + case 2: + if (moduleConfig.external_notification.output_buzzer) + digitalWrite(moduleConfig.external_notification.output_buzzer, true); + break; + default: + if (output > 0) + digitalWrite(output, (moduleConfig.external_notification.active ? true : false)); + break; + } + +#ifdef HAS_NCP5623 + if (rgb_found.type == ScanI2C::NCP5623) { + rgb.setColor(red, green, blue); + } +#endif +#ifdef RGBLED_CA + 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); +#endif +#ifdef HAS_NEOPIXEL + pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT); + pixels.show(); +#endif +#ifdef UNPHONE + unphone.rgb(red, green, blue); +#endif +#ifdef T_WATCH_S3 + drv.go(); +#endif +} + +void ExternalNotificationModule::setExternalOff(uint8_t index) +{ + externalCurrentState[index] = 0; + externalTurnedOn[index] = millis(); + + switch (index) { + case 1: +#ifdef UNPHONE + unphone.vibe(false); // the unPhone's vibration motor is on a i2c GPIO expander +#endif + if (moduleConfig.external_notification.output_vibra) + digitalWrite(moduleConfig.external_notification.output_vibra, false); + break; + case 2: + if (moduleConfig.external_notification.output_buzzer) + digitalWrite(moduleConfig.external_notification.output_buzzer, false); + break; + default: + if (output > 0) + digitalWrite(output, (moduleConfig.external_notification.active ? false : true)); + break; + } + +#if defined(HAS_NCP5623) || defined(RGBLED_RED) || defined(HAS_NEOPIXEL) || defined(UNPHONE) + red = 0; + green = 0; + blue = 0; +#ifdef HAS_NCP5623 + if (rgb_found.type == ScanI2C::NCP5623) { + rgb.setColor(red, green, blue); + } +#endif +#ifdef RGBLED_CA + 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); +#endif +#ifdef HAS_NEOPIXEL + pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT); + pixels.show(); +#endif +#ifdef UNPHONE + unphone.rgb(red, green, blue); +#endif +#endif + +#ifdef T_WATCH_S3 + drv.stop(); +#endif +} + +bool ExternalNotificationModule::getExternal(uint8_t index) +{ + return externalCurrentState[index]; +} + +void ExternalNotificationModule::stopNow() +{ + rtttl::stop(); +#ifdef HAS_I2S + if (audioThread->isPlaying()) + audioThread->stop(); +#endif + nagCycleCutoff = 1; // small value + isNagging = false; + setIntervalFromNow(0); +#ifdef T_WATCH_S3 + drv.stop(); +#endif +} + +ExternalNotificationModule::ExternalNotificationModule() + : SinglePortModule("ExternalNotificationModule", meshtastic_PortNum_TEXT_MESSAGE_APP), + concurrency::OSThread("ExternalNotificationModule") +{ + /* + 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.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; + + if (moduleConfig.external_notification.enabled) { + if (nodeDB->loadProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, sizeof(meshtastic_RTTTLConfig), + &meshtastic_RTTTLConfig_msg, &rtttlConfig) != LoadFileResult::LOAD_SUCCESS) { + memset(rtttlConfig.ringtone, 0, sizeof(rtttlConfig.ringtone)); + strncpy(rtttlConfig.ringtone, + "24:d=32,o=5,b=565:f6,p,f6,4p,p,f6,p,f6,2p,p,b6,p,b6,p,b6,p,b6,p,b,p,b,p,b,p,b,p,b,p,b,p,b,p,b,1p.,2p.,p", + sizeof(rtttlConfig.ringtone)); + } + + LOG_INFO("Initializing External Notification Module"); + + output = moduleConfig.external_notification.output ? moduleConfig.external_notification.output + : EXT_NOTIFICATION_MODULE_OUTPUT; + + // Set the direction of a pin + if (output > 0) { + LOG_INFO("Using Pin %i in digital mode", output); + pinMode(output, OUTPUT); + } + setExternalOff(0); + externalTurnedOn[0] = 0; + if (moduleConfig.external_notification.output_vibra) { + LOG_INFO("Using Pin %i for vibra motor", moduleConfig.external_notification.output_vibra); + pinMode(moduleConfig.external_notification.output_vibra, OUTPUT); + setExternalOff(1); + externalTurnedOn[1] = 0; + } + if (moduleConfig.external_notification.output_buzzer) { + if (!moduleConfig.external_notification.use_pwm) { + LOG_INFO("Using Pin %i for buzzer", moduleConfig.external_notification.output_buzzer); + pinMode(moduleConfig.external_notification.output_buzzer, OUTPUT); + setExternalOff(2); + 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("Using Pin %i in PWM mode", config.device.buzzer_gpio); + } + } +#ifdef HAS_NCP5623 + if (rgb_found.type == ScanI2C::NCP5623) { + rgb.begin(); + rgb.setCurrent(10); + } +#endif +#ifdef RGBLED_RED + 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); +#endif +#ifdef HAS_NEOPIXEL + 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(); + } +} + +ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshPacket &mp) +{ + if (moduleConfig.external_notification.enabled && !isMuted) { +#ifdef T_WATCH_S3 + 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 (int i = 0; i < p.payload.size; i++) { + if (p.payload.bytes[i] == ASCII_BELL) { + containsBell = true; + } + } + + if (moduleConfig.external_notification.alert_bell) { + if (containsBell) { + LOG_INFO("externalNotificationModule - Notification Bell"); + isNagging = true; + setExternalOn(0); + 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; + setExternalOn(1); + 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) { + if (containsBell) { + LOG_INFO("externalNotificationModule - Notification Bell (Buzzer)"); + isNagging = true; + if (!moduleConfig.external_notification.use_pwm) { + setExternalOn(2); + } else { +#ifdef HAS_I2S + audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); +#else + rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); +#endif + } + 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) { + LOG_INFO("externalNotificationModule - Notification Module"); + isNagging = true; + setExternalOn(0); + 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) { + LOG_INFO("externalNotificationModule - Notification Module (Vibra)"); + isNagging = true; + setExternalOn(1); + 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) { + LOG_INFO("externalNotificationModule - Notification Module (Buzzer)"); + isNagging = true; + if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) { + setExternalOn(2); + } else { +#ifdef HAS_I2S + if (moduleConfig.external_notification.use_i2s_as_buzzer) { + audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); + } +#else + rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); +#endif + } + if (moduleConfig.external_notification.nag_timeout) { + nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; + } else { + nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; + } + } + 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 +} + +/** + * @brief An admin message arrived to AdminModule. We are asked whether we want to handle that. + * + * @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_RESULT if a result is also prepared. + */ +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 is getting ringtone"); + this->handleGetRingtone(mp, response); + result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE; + break; + + case meshtastic_AdminMessage_set_ringtone_message_tag: + LOG_INFO("Client is setting ringtone"); + this->handleSetRingtone(request->set_canned_message_module_messages); + result = AdminMessageHandleResult::HANDLED; + break; + + default: + result = AdminMessageHandleResult::NOT_HANDLED; + } + + 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::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 (changed) { + nodeDB->saveProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, &meshtastic_RTTTLConfig_msg, &rtttlConfig); + } +} \ No newline at end of file diff --git a/src/modules/ExternalNotificationModule.h b/src/modules/ExternalNotificationModule.h new file mode 100644 index 0000000..20d02d6 --- /dev/null +++ b/src/modules/ExternalNotificationModule.h @@ -0,0 +1,69 @@ +#pragma once + +#include "SinglePortModule.h" +#include "concurrency/OSThread.h" +#include "configuration.h" +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !defined(CONFIG_IDF_TARGET_ESP32C6) +#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; } +}; +#endif +#include +#include + +/* + * Radio interface for ExternalNotificationModule + * + */ +class ExternalNotificationModule : public SinglePortModule, private concurrency::OSThread +{ + uint32_t output = 0; + + public: + ExternalNotificationModule(); + + uint32_t nagCycleCutoff = 1; + + void setExternalOn(uint8_t index = 0); + void setExternalOff(uint8_t index = 0); + bool getExternal(uint8_t index = 0); + + void setMute(bool mute) { isMuted = mute; } + bool getMute() { return isMuted; } + + void stopNow(); + + 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; + + virtual int32_t runOnce() override; + + virtual bool wantPacket(const meshtastic_MeshPacket *p) override; + + bool isNagging = false; + + bool isMuted = false; + + 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/ModuleDev.h b/src/modules/ModuleDev.h new file mode 100644 index 0000000..da8de3e --- /dev/null +++ b/src/modules/ModuleDev.h @@ -0,0 +1,10 @@ +#pragma once + +/* + * To developers: + * Use this to enable / disable features in your module that you don't want to risk checking into GitHub. + * + */ + +// Enable development more for StoreForwardModule +bool StoreForward_Dev = false; \ No newline at end of file diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp new file mode 100644 index 0000000..ad3f0ac --- /dev/null +++ b/src/modules/Modules.cpp @@ -0,0 +1,248 @@ +#include "configuration.h" +#if !MESHTASTIC_EXCLUDE_INPUTBROKER +#include "input/ExpressLRSFiveWay.h" +#include "input/InputBroker.h" +#include "input/RotaryEncoderInterruptImpl1.h" +#include "input/ScanAndSelect.h" +#include "input/SerialKeyboardImpl.h" +#include "input/TrackballInterruptImpl1.h" +#include "input/UpDownInterruptImpl1.h" +#include "input/cardKbI2cImpl.h" +#include "input/kbMatrixImpl.h" +#endif +#if !MESHTASTIC_EXCLUDE_ADMIN +#include "modules/AdminModule.h" +#endif +#if !MESHTASTIC_EXCLUDE_ATAK +#include "modules/AtakPluginModule.h" +#endif +#if !MESHTASTIC_EXCLUDE_CANNEDMESSAGES +#include "modules/CannedMessageModule.h" +#endif +#if !MESHTASTIC_EXCLUDE_DETECTIONSENSOR +#include "modules/DetectionSensorModule.h" +#endif +#if !MESHTASTIC_EXCLUDE_NEIGHBORINFO +#include "modules/NeighborInfoModule.h" +#endif +#if !MESHTASTIC_EXCLUDE_NODEINFO +#include "modules/NodeInfoModule.h" +#endif +#if !MESHTASTIC_EXCLUDE_GPS +#include "modules/PositionModule.h" +#endif +#if !MESHTASTIC_EXCLUDE_REMOTEHARDWARE +#include "modules/RemoteHardwareModule.h" +#endif +#if !MESHTASTIC_EXCLUDE_POWERSTRESS +#include "modules/PowerStressModule.h" +#endif +#include "modules/RoutingModule.h" +#include "modules/TextMessageModule.h" +#if !MESHTASTIC_EXCLUDE_TRACEROUTE +#include "modules/TraceRouteModule.h" +#endif +#if !MESHTASTIC_EXCLUDE_WAYPOINT +#include "modules/WaypointModule.h" +#endif +#if ARCH_PORTDUINO +#include "input/LinuxInputImpl.h" +#if !MESHTASTIC_EXCLUDE_STOREFORWARD +#include "modules/StoreForwardModule.h" +#endif +#endif +#if HAS_TELEMETRY +#include "modules/Telemetry/DeviceTelemetry.h" +#endif +#if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#include "main.h" +#include "modules/Telemetry/AirQualityTelemetry.h" +#include "modules/Telemetry/EnvironmentTelemetry.h" +#include "modules/Telemetry/HealthTelemetry.h" +#endif +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY +#include "modules/Telemetry/PowerTelemetry.h" +#endif +#ifdef ARCH_ESP32 +#if defined(USE_SX1280) && !MESHTASTIC_EXCLUDE_AUDIO +#include "modules/esp32/AudioModule.h" +#endif +#if !MESHTASTIC_EXCLUDE_PAXCOUNTER +#include "modules/esp32/PaxcounterModule.h" +#endif +#if !MESHTASTIC_EXCLUDE_STOREFORWARD +#include "modules/StoreForwardModule.h" +#endif +#endif +#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) +#if !MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION +#include "modules/ExternalNotificationModule.h" +#endif +#if !MESHTASTIC_EXCLUDE_RANGETEST && !MESHTASTIC_EXCLUDE_GPS +#include "modules/RangeTestModule.h" +#endif +#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_SERIAL +#include "modules/SerialModule.h" +#endif +#endif + +#if !MESHTASTIC_EXCLUDE_DROPZONE +#include "modules/DropzoneModule.h" +#endif + +/** + * Create module instances here. If you are adding a new module, you must 'new' it here (or somewhere else) + */ +void setupModules() +{ + if (config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER) { +#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER + inputBroker = new InputBroker(); +#endif +#if !MESHTASTIC_EXCLUDE_ADMIN + adminModule = new AdminModule(); +#endif +#if !MESHTASTIC_EXCLUDE_NODEINFO + nodeInfoModule = new NodeInfoModule(); +#endif +#if !MESHTASTIC_EXCLUDE_GPS + positionModule = new PositionModule(); +#endif +#if !MESHTASTIC_EXCLUDE_WAYPOINT + waypointModule = new WaypointModule(); +#endif +#if !MESHTASTIC_EXCLUDE_TEXTMESSAGE + textMessageModule = new TextMessageModule(); +#endif +#if !MESHTASTIC_EXCLUDE_TRACEROUTE + traceRouteModule = new TraceRouteModule(); +#endif +#if !MESHTASTIC_EXCLUDE_NEIGHBORINFO + neighborInfoModule = new NeighborInfoModule(); +#endif +#if !MESHTASTIC_EXCLUDE_DETECTIONSENSOR + detectionSensorModule = new DetectionSensorModule(); +#endif +#if !MESHTASTIC_EXCLUDE_ATAK + atakPluginModule = new AtakPluginModule(); +#endif + +#if !MESHTASTIC_EXCLUDE_DROPZONE + dropzoneModule = new DropzoneModule(); +#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. + +#if !MESHTASTIC_EXCLUDE_REMOTEHARDWARE + new RemoteHardwareModule(); +#endif +#if !MESHTASTIC_EXCLUDE_POWERSTRESS + new PowerStressModule(); +#endif + // Example: Put your module here + // new ReplyModule(); +#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER + rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1(); + if (!rotaryEncoderInterruptImpl1->init()) { + delete rotaryEncoderInterruptImpl1; + rotaryEncoderInterruptImpl1 = nullptr; + } + upDownInterruptImpl1 = new UpDownInterruptImpl1(); + if (!upDownInterruptImpl1->init()) { + delete upDownInterruptImpl1; + upDownInterruptImpl1 = nullptr; + } + +#if HAS_SCREEN + // In order to have the user button dismiss the canned message frame, this class lightly interacts with the Screen class + scanAndSelectInput = new ScanAndSelectInput(); + if (!scanAndSelectInput->init()) { + delete scanAndSelectInput; + scanAndSelectInput = nullptr; + } +#endif + + cardKbI2cImpl = new CardKbI2cImpl(); + cardKbI2cImpl->init(); +#ifdef INPUTBROKER_MATRIX_TYPE + kbMatrixImpl = new KbMatrixImpl(); + kbMatrixImpl->init(); +#endif // INPUTBROKER_MATRIX_TYPE +#ifdef INPUTBROKER_SERIAL_TYPE + aSerialKeyboardImpl = new SerialKeyboardImpl(); + aSerialKeyboardImpl->init(); +#endif // INPUTBROKER_MATRIX_TYPE +#endif // HAS_BUTTON +#if ARCH_PORTDUINO + aLinuxInputImpl = new LinuxInputImpl(); + aLinuxInputImpl->init(); +#endif +#if HAS_TRACKBALL && !MESHTASTIC_EXCLUDE_INPUTBROKER + trackballInterruptImpl1 = new TrackballInterruptImpl1(); + trackballInterruptImpl1->init(); +#endif +#ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE + expressLRSFiveWayInput = new ExpressLRSFiveWay(); +#endif +#if HAS_SCREEN && !MESHTASTIC_EXCLUDE_CANNEDMESSAGES + cannedMessageModule = new CannedMessageModule(); +#endif +#if HAS_TELEMETRY + new DeviceTelemetryModule(); +#endif +#if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + new EnvironmentTelemetryModule(); + if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) { + new AirQualityTelemetryModule(); + } + if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX30102].first > 0 || + nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MLX90614].first > 0) { + new HealthTelemetryModule(); + } +#endif +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + new PowerTelemetryModule(); +#endif +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) +#if !MESHTASTIC_EXCLUDE_SERIAL + new SerialModule(); +#endif +#endif +#ifdef ARCH_ESP32 + // Only run on an esp32 based device. +#if defined(USE_SX1280) && !MESHTASTIC_EXCLUDE_AUDIO + audioModule = new AudioModule(); +#endif +#if !MESHTASTIC_EXCLUDE_PAXCOUNTER + paxcounterModule = new PaxcounterModule(); +#endif +#endif +#if defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) +#if !MESHTASTIC_EXCLUDE_STOREFORWARD + storeForwardModule = new StoreForwardModule(); +#endif +#endif +#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) +#if !MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION + externalNotificationModule = new ExternalNotificationModule(); +#endif +#if !MESHTASTIC_EXCLUDE_RANGETEST && !MESHTASTIC_EXCLUDE_GPS + new RangeTestModule(); +#endif +#endif + } else { +#if !MESHTASTIC_EXCLUDE_ADMIN + adminModule = new AdminModule(); +#endif +#if HAS_TELEMETRY + new DeviceTelemetryModule(); +#endif +#if !MESHTASTIC_EXCLUDE_TRACEROUTE + traceRouteModule = new TraceRouteModule(); +#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(); +} \ No newline at end of file diff --git a/src/modules/Modules.h b/src/modules/Modules.h new file mode 100644 index 0000000..e34fc1e --- /dev/null +++ b/src/modules/Modules.h @@ -0,0 +1,6 @@ +#pragma once + +/** + * Create module instances here. If you are adding a new module, you must 'new' it here (or somewhere else) + */ +void setupModules(); \ No newline at end of file diff --git a/src/modules/NeighborInfoModule.cpp b/src/modules/NeighborInfoModule.cpp new file mode 100644 index 0000000..cbf63e9 --- /dev/null +++ b/src/modules/NeighborInfoModule.cpp @@ -0,0 +1,213 @@ +#include "NeighborInfoModule.h" +#include "Default.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "RTC.h" +#include + +NeighborInfoModule *neighborInfoModule; + +/* +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); + } +} + +/* +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); + } +} + +/* 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("NeighborInfoModule") +{ + 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(); + } +} + +/* +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); + + 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++; + } + } + 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("Removing 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); + meshtastic_MeshPacket *p = allocDataProtobuf(neighborInfo); + // send regardless of whether or not we have neighbors in our DB, + // because we want to get neighbors for the next cycle + 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 (airTime->isTxAllowedChannelUtil(true) && airTime->isTxAllowedAirUtil()) { + sendNeighborInfo(NODENUM_BROADCAST_NO_LORA, false); + } + return Default::getConfiguredOrDefaultMs(moduleConfig.neighbor_info.update_interval, default_neighbor_info_broadcast_secs); +} + +/* +Collect a recieved 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) +{ + if (np) { + printNeighborInfo("RECEIVED", np); + updateNeighbors(mp, np); + } else if (mp.hop_start != 0 && mp.hop_start == mp.hop_limit) { + // 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(); + + // 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) +{ + // 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]; + } + } + // 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, replacing oldest neighbor"); + neighbors.erase(neighbors.begin()); + neighbors.push_back(new_nbr); + } + return &neighbors.back(); +} \ No newline at end of file diff --git a/src/modules/NeighborInfoModule.h b/src/modules/NeighborInfoModule.h new file mode 100644 index 0000000..aa76a21 --- /dev/null +++ b/src/modules/NeighborInfoModule.h @@ -0,0 +1,70 @@ +#pragma once +#include "ProtobufModule.h" +#define MAX_NUM_NEIGHBORS 10 // also defined in NeighborInfo protobuf options + +/* + * 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); + + std::vector neighbors; + + public: + /* + * Expose the constructor + */ + NeighborInfoModule(); + + /* 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; + + /* + * 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(); + + /* 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); + + /* + * 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 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; + + /* 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(); +}; +extern NeighborInfoModule *neighborInfoModule; \ No newline at end of file diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp new file mode 100644 index 0000000..8437118 --- /dev/null +++ b/src/modules/NodeInfoModule.cpp @@ -0,0 +1,109 @@ +#include "NodeInfoModule.h" +#include "Default.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "RTC.h" +#include "Router.h" +#include "configuration.h" +#include "main.h" +#include + +NodeInfoModule *nodeInfoModule; + +bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *pptr) +{ + auto p = *pptr; + + bool hasChanged = nodeDB->updateUser(getFrom(&mp), p, mp.channel); + + bool wasBroadcast = isBroadcast(mp.to); + + // Show new nodes on LCD screen + if (wasBroadcast) { + String lcd = String("Joined: ") + p.long_name + "\n"; + if (screen) + screen->print(lcd.c_str()); + } + + // if user has changed while packet was not for us, inform phone + if (hasChanged && !wasBroadcast && !isToUs(&mp)) + service->sendToPhone(packetPool.allocCopy(mp)); + + // LOG_DEBUG("did handleReceived"); + return false; // Let others look at this message also if they want +} + +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; + meshtastic_MeshPacket *p = allocReply(); + if (p) { // Check whether we didn't ignore it + p->to = dest; + p->decoded.want_response = (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && + config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && + wantReplies; + if (_shorterTimeout) + p->priority = meshtastic_MeshPacket_Priority_DEFAULT; + else + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + if (channel > 0) { + LOG_DEBUG("sending ourNodeInfo to channel %d", channel); + p->channel = channel; + } + + prevPacketId = p->id; + + service->sendToMesh(p); + shorterTimeout = false; + } +} + +meshtastic_MeshPacket *NodeInfoModule::allocReply() +{ + if (!airTime->isTxAllowedChannelUtil(false)) { + ignoreRequest = true; // Mark it as ignored for MeshModule + LOG_DEBUG("Skip sending NodeInfo due to > 40 percent channel 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 sending NodeInfo since we just sent it less than 5 minutes ago."); + ignoreRequest = true; // Mark it as ignored for MeshModule + return NULL; + } else if (shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, 60 * 1000)) { + LOG_DEBUG("Skip sending actively requested NodeInfo since we just sent it less than 60 seconds ago."); + ignoreRequest = true; // Mark it as ignored for MeshModule + return NULL; + } else { + ignoreRequest = false; // Don't ignore requests anymore + meshtastic_User &u = owner; + + LOG_INFO("sending owner %s/%s/%s", u.id, u.long_name, u.short_name); + lastSentToMesh = millis(); + return allocDataProtobuf(u); + } +} + +NodeInfoModule::NodeInfoModule() + : ProtobufModule("nodeinfo", meshtastic_PortNum_NODEINFO_APP, &meshtastic_User_msg), concurrency::OSThread("NodeInfoModule") +{ + isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others + setIntervalFromNow(30 * + 1000); // 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; + + if (airTime->isTxAllowedAirUtil() && config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN) { + LOG_INFO("Sending 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 new file mode 100644 index 0000000..c1fb9cc --- /dev/null +++ b/src/modules/NodeInfoModule.h @@ -0,0 +1,45 @@ +#pragma once +#include "ProtobufModule.h" + +/** + * 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; + + uint32_t currentGeneration = 0; + + 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); + + 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; + + /** 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; + + private: + uint32_t lastSentToMesh = 0; // Last time we sent our NodeInfo to the mesh + bool shorterTimeout = false; +}; + +extern NodeInfoModule *nodeInfoModule; diff --git a/src/modules/PositionModule.cpp b/src/modules/PositionModule.cpp new file mode 100644 index 0000000..5500a55 --- /dev/null +++ b/src/modules/PositionModule.cpp @@ -0,0 +1,498 @@ +#if !MESHTASTIC_EXCLUDE_GPS +#include "PositionModule.h" +#include "Default.h" +#include "GPS.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "RTC.h" +#include "Router.h" +#include "TypeConversions.h" +#include "airtime.h" +#include "configuration.h" +#include "gps/GeoCoord.h" +#include "main.h" +#include "mesh/compression/unishox2.h" +#include "meshUtils.h" +#include "meshtastic/atak.pb.h" +#include "sleep.h" +#include "target_specific.h" + +extern "C" { +#include +} + +PositionModule *positionModule; + +PositionModule::PositionModule() + : ProtobufModule("position", meshtastic_PortNum_POSITION_APP, &meshtastic_Position_msg), + concurrency::OSThread("PositionModule") +{ + 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(60 * 1000); + + // 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("Clearing position on startup for sleepy tracker (ー。ー) zzz"); + nodeDB->clearLocalPosition(); + } +} + +bool PositionModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Position *pptr) +{ + auto p = *pptr; + + // If inbound message is a replay (or spoof!) of our own messages, we shouldn't process + // (why use second-hand sources for our own data?) + + // 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); + } + } + + // 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; + } else { + precision = 0; + } + + return false; // Let others look at this message also if they want +} + +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("Truncating 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("Ignoring 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("Ignoring 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 = (gps && gps->isConnected()) || (rtc_found.address != ScanI2C::ADDRESS_NONE.address); +#endif + return hasGpsOrRtc || setFromPhoneOrNtpToday; +} + +meshtastic_MeshPacket *PositionModule::allocReply() +{ + if (precision == 0) { + LOG_DEBUG("Skipping 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); + + // 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++; + + if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) { + LOG_WARN("Skipping position send because lat/lon are zero!"); + return nullptr; + } + + // lat/lon are unconditionally included - IF AVAILABLE! + LOG_DEBUG("Sending 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; + p.time = getValidTime(RTCQualityNTP) > 0 ? getValidTime(RTCQualityNTP) : localPosition.time; + + if (config.position.fixed_position) { + p.location_source = meshtastic_Position_LocSource_LOC_MANUAL; + } + + 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_GEOIDAL_SEPARATION) { + p.altitude_geoidal_separation = localPosition.altitude_geoidal_separation; + p.has_altitude_geoidal_separation = 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_SATINVIEW) + p.sats_in_view = localPosition.sats_in_view; + + if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_TIMESTAMP) + p.timestamp = localPosition.timestamp; + + if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_SEQ_NO) + p.seq_number = localPosition.seq_number; + + 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; + } + + // Strip out any time information before sending packets to other nodes - to keep the wire size small (and because other + // nodes shouldn't trust it anyways) Note: we allow a device with a local GPS or NTP to include the time, so that devices + // without can get time. + if (getRTCQuality() < RTCQualityNTP) { + LOG_INFO("Stripping time %u from position send", p.time); + p.time = 0; + } else { + p.time = getValidTime(RTCQualityNTP); + LOG_INFO("Providing time to mesh %u", p.time); + } + + LOG_INFO("Position reply: time=%i lat=%i lon=%i", p.time, p.latitude_i, p.longitude_i); + + // 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(); + + return allocDataProtobuf(p); +} + +meshtastic_MeshPacket *PositionModule::allocAtakPli() +{ + LOG_INFO("Sending 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("Sending pos@%x:6 to mesh (wantReplies=%d)", localPosition.timestamp, requestReplies); + sendOurPosition(NODENUM_BROADCAST, requestReplies); +} + +void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t channel) +{ + // 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; + } else if (channels.getByIndex(channel).role == meshtastic_Channel_Role_PRIMARY) { + // backwards compatibility for Primary channels created before position_precision was set by default + precision = 13; + } else { + precision = 0; + } + + meshtastic_MeshPacket *p = allocReply(); + if (p == nullptr) { + LOG_DEBUG("allocReply 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) { + LOG_DEBUG("Starting next execution in 5 seconds and then going to sleep."); + sleepOnNextExecution = true; + setIntervalFromNow(5000); + } +} + +#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("Sleeping for %ims, then awaking to send position again.", nightyNightMs); + doDeepSleep(nightyNightMs, false); + } + + 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; + } + + if (lastGpsSend == 0 || msSinceLastSend >= intervalMs) { + if (hasValidPosition(node)) { + lastGpsSend = now; + + 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 (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("Skipping 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); + + service->sendToMesh(p, RX_SRC_LOCAL, true); + delete[] message; +} + +struct SmartPosition PositionModule::getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition) +{ + // The minimum distance to travel before we are able to send a new position packet. + const uint32_t distanceTravelThreshold = + Default::getConfiguredOrDefault(config.position.broadcast_smart_minimum_distance, 100); + + // Determine the distance in meters between two points on the globe + float distanceTraveledSinceLastSend = GeoCoord::latLongToMeter( + lastGpsLatitude * 1e-7, lastGpsLongitude * 1e-7, currentPosition.latitude_i * 1e-7, currentPosition.longitude_i * 1e-7); + +#ifdef GPS_EXTRAVERBOSE + LOG_DEBUG("--------LAST POSITION------------------------------------"); + LOG_DEBUG("lastGpsLatitude=%i, lastGpsLatitude=%i", lastGpsLatitude, lastGpsLongitude); + + LOG_DEBUG("--------CURRENT POSITION---------------------------------"); + LOG_DEBUG("currentPosition.latitude_i=%i, currentPosition.longitude_i=%i", lastGpsLatitude, lastGpsLongitude); + + LOG_DEBUG("--------SMART POSITION-----------------------------------"); + LOG_DEBUG("hasTraveledOverThreshold=%i, distanceTraveled=%f, distanceThreshold=%f", + abs(distanceTraveledSinceLastSend) >= distanceTravelThreshold, abs(distanceTraveledSinceLastSend), + distanceTravelThreshold); + + if (abs(distanceTraveledSinceLastSend) >= distanceTravelThreshold) { + LOG_DEBUG("SMART SEEEEEEEEENDING"); + } +#endif + + return SmartPosition{.distanceTraveled = abs(distanceTraveledSinceLastSend), + .distanceThreshold = distanceTravelThreshold, + .hasTraveledOverThreshold = abs(distanceTraveledSinceLastSend) >= 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 (hasValidPosition(node2)) { + auto smartPosition = getDistanceTraveledSinceLastSend(node->position); + uint32_t msSinceLastSend = millis() - lastGpsSend; + if (smartPosition.hasTraveledOverThreshold && + Throttle::execute( + &lastGpsSend, minimumTimeThreshold, []() { positionModule->sendOurPosition(); }, + []() { LOG_DEBUG("Skipping 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 new file mode 100644 index 0000000..41b86b7 --- /dev/null +++ b/src/modules/PositionModule.h @@ -0,0 +1,75 @@ +#pragma once +#include "Default.h" +#include "ProtobufModule.h" +#include "concurrency/OSThread.h" + +/** + * Position module for sending/receiving positions into the mesh + */ +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; + + /// 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; + + /// We force a rebroadcast if the radio settings change + uint32_t currentGeneration = 0; + + 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(); + + void handleNewPosition(); + + 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; + + 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; + + /** Does our periodic broadcast */ + virtual int32_t runOnce() override; + + private: + 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(); + + const uint32_t minimumTimeThreshold = + Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30); +}; + +struct SmartPosition { + 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 new file mode 100644 index 0000000..5605d11 --- /dev/null +++ b/src/modules/PowerStressModule.cpp @@ -0,0 +1,134 @@ +#include "PowerStressModule.h" +#include "Led.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "PowerMon.h" +#include "RTC.h" +#include "Router.h" +#include "configuration.h" +#include "main.h" +#include "sleep.h" +#include "target_specific.h" +#include + +extern void printInfo(); + +PowerStressModule::PowerStressModule() + : ProtobufModule("powerstress", meshtastic_PortNum_POWERSTRESS_APP, &meshtastic_PowerStressMessage_msg), + concurrency::OSThread("PowerStressModule") +{ +} + +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; +} + +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); + 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 new file mode 100644 index 0000000..2d449f6 --- /dev/null +++ b/src/modules/PowerStressModule.h @@ -0,0 +1,38 @@ +#pragma once +#include "ProtobufModule.h" +#include "concurrency/OSThread.h" +#include "mesh/generated/meshtastic/powermon.pb.h" + +/** + * 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; + + public: + /** Constructor + * name is for debugging output + */ + PowerStressModule(); + + 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; + + /** + * 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 new file mode 100644 index 0000000..2deb2ba --- /dev/null +++ b/src/modules/RangeTestModule.cpp @@ -0,0 +1,295 @@ +/** + * @file RangeTestModule.cpp + * @brief Implementation of the RangeTestModule class and RangeTestModuleRadio class. + * + * As a sender, this module sends packets every n seconds with an incremented PacketID. + * As a receiver, this module receives packets from multiple senders and saves them to the Filesystem. + * + * The RangeTestModule class is an OSThread that runs the module. + * The RangeTestModuleRadio class handles sending and receiving packets. + */ +#include "RangeTestModule.h" +#include "FSCommon.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "PowerFSM.h" +#include "RTC.h" +#include "Router.h" +#include "airtime.h" +#include "configuration.h" +#include "gps/GeoCoord.h" +#include +#include + +RangeTestModule *rangeTestModule; +RangeTestModuleRadio *rangeTestModuleRadio; + +RangeTestModule::RangeTestModule() : concurrency::OSThread("RangeTestModule") {} + +uint32_t packetSequence = 0; + +int32_t RangeTestModule::runOnce() +{ +#if defined(ARCH_ESP32) || defined(ARCH_NRF52) + + /* + 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; + + // 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) { + + if (firstTime) { + rangeTestModuleRadio = new RangeTestModuleRadio(); + + firstTime = 0; + + if (moduleConfig.range_test.sender) { + LOG_INFO("Initializing 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("Initializing 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 - Disabling 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(); +} + +/** + * Sends a payload to a specified destination node. + * + * @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; + + 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); + + service->sendToMesh(p); + + // 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) +{ +#if defined(ARCH_ESP32) || defined(ARCH_NRF52) + + 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.hop_limit %d", mp.hop_limit); + // LOG_DEBUG("mp.decoded.position.latitude_i %d", mp.decoded.position.latitude_i); // Deprecated + // LOG_DEBUG("mp.decoded.position.longitude_i %d", mp.decoded.position.longitude_i); // Deprecated + 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("mp.decoded.position.latitude_i %d", mp.decoded.position.latitude_i); // Deprecated + // LOG_DEBUG("mp.decoded.position.longitude_i %d", mp.decoded.position.longitude_i); // Deprecated + 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("-----------------------------------------"); + */ + 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")) { + 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 + } 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 + 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 + + 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.flush(); + fileToAppend.close(); +#endif + + return 1; +} \ No newline at end of file diff --git a/src/modules/RangeTestModule.h b/src/modules/RangeTestModule.h new file mode 100644 index 0000000..b632d34 --- /dev/null +++ b/src/modules/RangeTestModule.h @@ -0,0 +1,56 @@ +#pragma once + +#include "SinglePortModule.h" +#include "concurrency/OSThread.h" +#include "configuration.h" +#include +#include + +class RangeTestModule : private concurrency::OSThread +{ + bool firstTime = 1; + unsigned long started = 0; + + public: + RangeTestModule(); + + protected: + virtual int32_t runOnce() override; +}; + +extern RangeTestModule *rangeTestModule; + +/* + * Radio interface for RangeTestModule + * + */ +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 + } + + /** + * 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); + + 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; +}; + +extern RangeTestModuleRadio *rangeTestModuleRadio; diff --git a/src/modules/RemoteHardwareModule.cpp b/src/modules/RemoteHardwareModule.cpp new file mode 100644 index 0000000..a7d81cd --- /dev/null +++ b/src/modules/RemoteHardwareModule.cpp @@ -0,0 +1,168 @@ +#include "RemoteHardwareModule.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "RTC.h" +#include "Router.h" +#include "configuration.h" +#include "main.h" +#include + +#define NUM_GPIOS 64 + +// Because (FIXME) we currently don't tell API clients status on sent messages +// we need to throttle our sending, so that if a gpio is bouncing up and down we +// don't generate more messages than the net can send. So we limit watch messages to +// a max of one change per 30 seconds +#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; + } + + // 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; +} + +/// 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); + } + } + } +} + +/// Read all the pins mentioned in a mask +static uint64_t digitalReads(uint64_t mask, uint64_t maskAvailable) +{ + uint64_t res = 0; + + 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; + } + } + } + + return res; +} + +RemoteHardwareModule::RemoteHardwareModule() + : ProtobufModule("remotehardware", meshtastic_PortNum_REMOTE_HARDWARE_APP, &meshtastic_HardwareMessage_msg), + concurrency::OSThread("RemoteHardwareModule") +{ + // 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; + } +} + +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: { + // Print notification to LCD screen + screen->print("Write GPIOs\n"); + + 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; + } + + case meshtastic_HardwareMessage_Type_READ_GPIOS: { + // Print notification to LCD screen + if (screen) + screen->print("Read GPIOs\n"); + + 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) { + + if (!Throttle::isWithinTimespanMs(lastWatchMsec, WATCH_INTERVAL_MSEC)) { + uint64_t curVal = digitalReads(watchGpios, availablePins); + lastWatchMsec = millis(); + + if (curVal != previousWatch) { + previousWatch = curVal; + LOG_INFO("Broadcasting 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(); + } + + 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 new file mode 100644 index 0000000..4dc31d4 --- /dev/null +++ b/src/modules/RemoteHardwareModule.h @@ -0,0 +1,47 @@ +#pragma once +#include "ProtobufModule.h" +#include "concurrency/OSThread.h" +#include "mesh/generated/meshtastic/remote_hardware.pb.h" + +/** + * 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; + + /// 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; + + /// 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(); + + 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; + + /** + * 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 new file mode 100644 index 0000000..27a12d2 --- /dev/null +++ b/src/modules/ReplyModule.cpp @@ -0,0 +1,26 @@ +#include "ReplyModule.h" +#include "MeshService.h" +#include "configuration.h" +#include "main.h" + +#include + +meshtastic_MeshPacket *ReplyModule::allocReply() +{ + assert(currentRequest); // should always be !NULL +#ifdef DEBUG_PORT + 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 + + screen->print("Sending reply\n"); + + 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; +} diff --git a/src/modules/ReplyModule.h b/src/modules/ReplyModule.h new file mode 100644 index 0000000..86d4172 --- /dev/null +++ b/src/modules/ReplyModule.h @@ -0,0 +1,20 @@ +#pragma once +#include "SinglePortModule.h" + +/** + * 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) {} + + 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 new file mode 100644 index 0000000..f11a9a5 --- /dev/null +++ b/src/modules/RoutingModule.cpp @@ -0,0 +1,85 @@ +#include "RoutingModule.h" +#include "Default.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "Router.h" +#include "configuration.h" +#include "main.h" + +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; + } + + 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() +{ + if (config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) + return NULL; + assert(currentRequest); + + // We only consider making replies if the request was a legit routing packet (not just something we were sniffing) + if (currentRequest->decoded.portnum == meshtastic_PortNum_ROUTING_APP) { + assert(0); // 1.2 refactoring fixme, Not sure if anything needs this yet? + // return allocDataProtobuf(u); + } + return NULL; +} + +void RoutingModule::sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopStart, + uint8_t hopLimit) +{ + auto p = allocAckNak(err, to, idFrom, chIndex, hopStart, hopLimit); + + router->sendLocal(p); // we sometimes send directly to the local node +} + +uint8_t RoutingModule::getHopLimitForResponse(uint8_t hopStart, uint8_t hopLimit) +{ + if (hopStart != 0) { + // Hops used by the request. If somebody in between running modified firmware modified it, ignore it + uint8_t hopsUsed = hopStart < hopLimit ? config.lora.hop_limit : hopStart - hopLimit; + if (hopsUsed > 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 +} + +RoutingModule::RoutingModule() : ProtobufModule("routing", meshtastic_PortNum_ROUTING_APP, &meshtastic_Routing_msg) +{ + isPromiscuous = true; + + // moved the ReboradcastMode 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 new file mode 100644 index 0000000..f085b30 --- /dev/null +++ b/src/modules/RoutingModule.h @@ -0,0 +1,39 @@ +#pragma once +#include "Channels.h" +#include "ProtobufModule.h" + +/** + * Routing module for router control messages + */ +class RoutingModule : public ProtobufModule +{ + public: + /** Constructor + * name is for debugging output + */ + RoutingModule(); + + void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopStart = 0, + 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(uint8_t hopStart, uint8_t hopLimit); + + protected: + friend class Router; + + /** 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; + + /** 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; } +}; + +extern RoutingModule *routingModule; diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp new file mode 100644 index 0000000..dc9c8aa --- /dev/null +++ b/src/modules/SerialModule.cpp @@ -0,0 +1,590 @@ +#include "SerialModule.h" +#include "GeoCoord.h" +#include "MeshService.h" +#include "NMEAWPL.h" +#include "NodeDB.h" +#include "RTC.h" +#include "Router.h" +#include "configuration.h" +#include +#include + +/* + SerialModule + A simple interface to send messages over the mesh network by sending strings + over a serial port. + + There are no PIN defaults, you have to enable the second serial port yourself. + + Need help with this module? Post your question on the Meshtastic Discourse: + https://meshtastic.discourse.group + + Basic Usage: + + 1) Enable the module by setting enabled to 1. + 2) Set the pins (rxd / rxd) for your preferred RX and TX GPIO pins. + On tbeam, recommend to use: + RXD 35 + TXD 15 + 3) Set timeout to the amount of time to wait before we consider + your packet as "done". + 4) not applicable any more + 5) Connect to your device over the serial interface at 38400 8N1. + 6) Send a packet up to 240 bytes in length. This will get relayed over the mesh network. + 7) (Optional) Set echo to 1 and any message you send out will be echoed back + to your device. + + TODO (in this order): + * Define a verbose RX mode to report on mesh and packet information. + - This won't happen any time soon. + + KNOWN PROBLEMS + * Until the module is initialized by the startup sequence, the TX pin is in a floating + state. Device connected to that pin may see this as "noise". + * Will not work on Linux device targets. + + +*/ + +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) + +#define RX_BUFFER 256 +#define TIMEOUT 250 +#define BAUD 38400 +#define ACK 1 + +// API: Defaulting to the formerly removed phone_timeout_secs value of 15 minutes +#define SERIAL_CONNECTION_TIMEOUT (15 * 60) * 1000UL + +SerialModule *serialModule; +SerialModuleRadio *serialModuleRadio; + +#if defined(TTGO_T_ECHO) || defined(CANARYONE) +SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("SerialModule") {} +static Print *serialPrint = &Serial; +#elif defined(CONFIG_IDF_TARGET_ESP32C6) +SerialModule::SerialModule() : StreamAPI(&Serial1), concurrency::OSThread("SerialModule") {} +static Print *serialPrint = &Serial1; +#else +SerialModule::SerialModule() : StreamAPI(&Serial2), concurrency::OSThread("SerialModule") {} +static Print *serialPrint = &Serial2; +#endif + +char serialBytes[512]; +size_t serialPayloadSize; + +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; + } +} + +/** + * @brief Checks if the serial connection is established. + * + * @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 + */ +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. + */ + + // 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.override_console_serial_port || (moduleConfig.serial.rxd && moduleConfig.serial.txd)) { + if (firstTime) { + // Interface with the serial peripheral from in here. + LOG_INFO("Initializing serial peripheral interface"); + + uint32_t baud = getBaudRate(); + + if (moduleConfig.serial.override_console_serial_port) { +#ifdef RP2040_SLOW_CLOCK + Serial2.flush(); + serialPrint = &Serial2; +#else + Serial.flush(); + serialPrint = &Serial; +#endif + // 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); + } + +#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(CANARYONE) + 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 && tempNodeInfo->has_user && 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(CANARYONE) + else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) { + processWXSerial(); + + } else { +#if defined(CONFIG_IDF_TARGET_ESP32C6) + while (Serial1.available()) { + serialPayloadSize = Serial1.readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN); +#else + while (Serial2.available()) { + serialPayloadSize = Serial2.readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN); +#endif + serialModuleRadio->sendPayload(); + } + } +#endif + } + return (10); + } else { + return disable(); + } +} + +/** + * Sends telemetry packet over the mesh network. + * + * @param m The telemetry data to be sent + * + * @return void + * + * @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); +} + +/** + * Allocates a new mesh packet for use as a reply to a received packet. + * + * @return A pointer to the newly allocated mesh packet. + */ +meshtastic_MeshPacket *SerialModuleRadio::allocReply() +{ + auto reply = allocDataPacket(); // Allocate a packet for sending + + return reply; +} + +/** + * Sends a payload to a specified destination node. + * + * @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; + + 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); + + service->sendToMesh(p); +} + +/** + * Handle a received mesh packet. + * + * @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)); + String 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 +} + +/** + * @brief Returns the baud rate of the serial module from the module configuration. + * + * @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; +} + +/** + * Process the received weather station serial data, extract wind, voltage, and temperature information, + * calculate averages and send telemetry data over the mesh network. + * + * @return void + */ +void SerialModule::processWXSerial() +{ +#if !defined(TTGO_T_ECHO) && !defined(CANARYONE) && !defined(CONFIG_IDF_TARGET_ESP32C6) + 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; + 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 + 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)); + memcpy(line, &serialBytes[lineStart], lineEnd - lineStart); + + if (strstr(line, "Wind") != NULL) // we have a wind line + { + gotwind = true; + // Find the positions of "=" signs in the line + char *windDirPos = strstr(line, "WindDir = "); + char *windSpeedPos = strstr(line, "WindSpeed = "); + char *windGustPos = strstr(line, "WindGust = "); + + if (windDirPos != NULL) { + // Extract data after "=" for WindDir + strcpy(windDir, windDirPos + 15); // Add 15 to skip "WindDir = " + double radians = toRadians(strtof(windDir, nullptr)); + dir_sum_sin += sin(radians); + dir_sum_cos += cos(radians); + dirCount++; + } else if (windSpeedPos != NULL) { + // Extract data after "=" for WindSpeed + strcpy(windVel, windSpeedPos + 15); // Add 15 to skip "WindSpeed = " + float newv = strtof(windVel, nullptr); + velSum += newv; + velCount++; + if (newv < lull || lull == -1) + lull = newv; + + } else if (windGustPos != NULL) { + strcpy(windGust, windGustPos + 15); // Add 15 to skip "WindSpeed = " + float newg = strtof(windGust, nullptr); + if (newg > gust) + gust = newg; + } + + // these are also voltage data we care about possibly + } else if (strstr(line, "BatVoltage") != NULL) { // we have a battVoltage line + char *batVoltagePos = strstr(line, "BatVoltage = "); + if (batVoltagePos != NULL) { + strcpy(batVoltage, batVoltagePos + 17); // 18 for ws 80, 17 for ws85 + batVoltageF = strtof(batVoltage, nullptr); + break; // last possible data we want so break + } + } else if (strstr(line, "CapVoltage") != NULL) { // we have a cappVoltage line + char *capVoltagePos = strstr(line, "CapVoltage = "); + if (capVoltagePos != NULL) { + strcpy(capVoltage, capVoltagePos + 17); // 18 for ws 80, 17 for ws85 + capVoltageF = strtof(capVoltage, nullptr); + } + // GXTS04Temp = 24.4 + } else if (strstr(line, "GXTS04Temp") != NULL) { // we have a temperature line + char *tempPos = strstr(line, "GXTS04Temp = "); + if (tempPos != NULL) { + strcpy(temperature, tempPos + 15); // 15 spaces for ws85 + temperatureF = strtof(temperature, 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 + } + } + } + if (gotwind) { + + LOG_INFO("WS85 : %i %.1fg%.1f %.1fv %.1fv %.1fC", atoi(windDir), strtof(windVel, nullptr), strtof(windGust, nullptr), + batVoltageF, capVoltageF, temperatureF); + } + if (gotwind && !Throttle::isWithinTimespanMs(lastAveraged, averageIntervalMillis)) { + // calulate 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 = toDegrees(avgRadians); + + if (dirAvg < 0) { + dirAvg += 360.0; + } + lastAveraged = millis(); + + // 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.wind_speed = velAvg; + m.variant.environment_metrics.has_wind_speed = true; + + m.variant.environment_metrics.wind_direction = dirAvg; + m.variant.environment_metrics.has_wind_direction = true; + + m.variant.environment_metrics.temperature = temperatureF; + m.variant.environment_metrics.has_temperature = 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_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; + + LOG_INFO("WS85 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; +} +#endif \ No newline at end of file diff --git a/src/modules/SerialModule.h b/src/modules/SerialModule.h new file mode 100644 index 0000000..fa86db2 --- /dev/null +++ b/src/modules/SerialModule.h @@ -0,0 +1,80 @@ +#pragma once + +#include "MeshModule.h" +#include "Router.h" +#include "SinglePortModule.h" +#include "concurrency/OSThread.h" +#include "configuration.h" +#include +#include + +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !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] = ""; + + public: + SerialModule(); + + protected: + virtual int32_t runOnce() 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(); +}; + +extern SerialModule *serialModule; + +/* + * Radio interface for SerialModule + * + */ +class SerialModuleRadio : public MeshModule +{ + uint32_t lastRxID = 0; + char outbuf[90] = ""; + + public: + SerialModuleRadio(); + + /** + * Send our payload into the mesh + */ + void sendPayload(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); + + protected: + virtual meshtastic_MeshPacket *allocReply() 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; + + meshtastic_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; + + return p; + } +}; + +extern SerialModuleRadio *serialModuleRadio; + +#endif \ No newline at end of file diff --git a/src/modules/StoreForwardModule.cpp b/src/modules/StoreForwardModule.cpp new file mode 100644 index 0000000..0395232 --- /dev/null +++ b/src/modules/StoreForwardModule.cpp @@ -0,0 +1,620 @@ +/** + * @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. + * + * 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] + */ +#include "StoreForwardModule.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "RTC.h" +#include "Router.h" +#include "Throttle.h" +#include "airtime.h" +#include "configuration.h" +#include "memGet.h" +#include "mesh-pb-constants.h" +#include "mesh/generated/meshtastic/storeforward.pb.h" +#include "modules/ModuleDev.h" +#include +#include +#include + +StoreForwardModule *storeForwardModule; + +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("Sending 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(); +} + +/** + * 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 + */ + + LOG_DEBUG("Before PSRAM init: heap %d/%d PSRAM %d/%d", memGet.getFreeHeap(), memGet.getHeapSize(), memGet.getFreePsram(), + memGet.getPsramSize()); + + /* Use a maximum of 2/3 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() / 3) * 2) / sizeof(PacketHistoryStruct))); + this->records = numberOfPackets; +#if defined(ARCH_ESP32) + this->packetHistory = static_cast(ps_calloc(numberOfPackets, sizeof(PacketHistoryStruct))); +#elif defined(ARCH_PORTDUINO) + 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); +} + +/** + * Sends messages from the message history to the specified recipient. + * + * @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; + + if (queueSize) { + LOG_INFO("S&F - Sending %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 +} + +/** + * Returns the number of available packets in the message history for a specified destination node. + * + * @param dest The destination node number. + * @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++; + } + } + } + return count; +} + +/** + * Allocates a mesh packet for sending to the phone. + * + * @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; + } + } + return nullptr; +} + +/** + * Adds a mesh packet to the history buffer for store-and-forward functionality. + * + * @param mp The mesh packet to add to the history buffer. + */ +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 + } + } + + 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].payload_size = p.payload.size; + memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, meshtastic_Constants_DATA_PAYLOAD_LEN); + + this->packetHistoryTotalCount++; +} + +/** + * Sends a payload to a specified destination node using the store and forward mechanism. + * + * @param dest The destination node number. + * @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("Sending S&F Payload"); + service->sendToMesh(p); + this->requestCount++; + return true; + } + return false; +} + +/** + * Prepares a payload to be sent to a specified destination node from the S&F packet history. + * + * @param dest The destination node number. + * @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 *p = allocDataPacket(); + + p->to = local ? this->packetHistory[i].to : dest; // PhoneAPI can handle original `to` + p->from = this->packetHistory[i].from; + p->channel = this->packetHistory[i].channel; + p->rx_time = this->packetHistory[i].time; + + // 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; + } + + 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; +} + +/** + * Sends a message to a specified destination node using the store and forward protocol. + * + * @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); + + p->to = dest; + + 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; + + service->sendToMesh(p); +} + +/** + * Sends a store-and-forward message to the specified destination node. + * + * @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); +} + +/** + * Sends a text message with an error (busy or channel not available) to the specified destination node. + * + * @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); +} + +/** + * Sends statistics about the store and forward module to the specified node. + * + * @param to The node ID to send the statistics to. + */ +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; + + LOG_DEBUG("Sending S&F Stats"); + storeForwardModule->sendMessage(to, sf); +} + +/** + * Handles a received mesh packet, potentially storing it for later forwarding. + * + * @param mp The received mesh packet. + * @return A `ProcessMessage` indicating whether the packet was successfully handled. + */ +ProcessMessage StoreForwardModule::handleReceived(const meshtastic_MeshPacket &mp) +{ +#if defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) + 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"); + + // 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 protobuf 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 +} + +/** + * Handles a received protobuf message for the Store and Forward module. + * + * @param mp The received MeshPacket to handle. + * @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; + } + } + 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 +} + +StoreForwardModule::StoreForwardModule() + : concurrency::OSThread("StoreForwardModule"), + ProtobufModule("StoreForward", meshtastic_PortNum_STORE_FORWARD_APP, &meshtastic_StoreAndForward_msg) +{ + +#if defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) + + 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. + */ + + 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("Initializing 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, disabling."); + } + } else { + LOG_INFO("S&F: device doesn't have PSRAM, disabling."); + } + + // Client + } else { + is_client = true; + LOG_INFO("Initializing Store & Forward Module in Client mode"); + } + } else { + disable(); + } +#endif +} \ No newline at end of file diff --git a/src/modules/StoreForwardModule.h b/src/modules/StoreForwardModule.h new file mode 100644 index 0000000..e327347 --- /dev/null +++ b/src/modules/StoreForwardModule.h @@ -0,0 +1,108 @@ +#pragma once + +#include "ProtobufModule.h" +#include "concurrency/OSThread.h" +#include "mesh/generated/meshtastic/storeforward.pb.h" + +#include "configuration.h" +#include +#include +#include + +struct PacketHistoryStruct { + uint32_t time; + uint32_t to; + uint32_t from; + uint8_t channel; + uint8_t payload[meshtastic_Constants_DATA_PAYLOAD_LEN]; + pb_size_t payload_size; +}; + +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; + + uint32_t packetTimeMax = 5000; // Interval between sending history packets as a server. + + bool is_client = false; + bool is_server = false; + + // Unordered_map stores the last request for each nodeNum (`to` field) + std::unordered_map lastRequest; + + public: + StoreForwardModule(); + + 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); + + /** + * 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; + } + } + + 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. + + // 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). + + protected: + virtual int32_t runOnce() 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; + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_StoreAndForward *p); +}; + +extern StoreForwardModule *storeForwardModule; \ No newline at end of file diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp new file mode 100644 index 0000000..5f7fe5d --- /dev/null +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -0,0 +1,193 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "AirQualityTelemetry.h" +#include "Default.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "PowerFSM.h" +#include "RTC.h" +#include "Router.h" +#include "detect/ScanI2CTwoWire.h" +#include "main.h" +#include + +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; + + 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: Initializing"); + if (!aqi.begin_I2C()) { +#ifndef I2C_NO_RESCAN + LOG_WARN("Could not establish i2c connection to AQI sensor. Rescanning..."); + // 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 1000; + } +#endif + return disable(); + } + return 1000; + } + 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(); + + 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); + } + } + return sendToPhoneIntervalMs; +} + +bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) +{ + if (t->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { +#ifdef DEBUG_PORT + 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); +#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("Skipping 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.pm10_standard = data.pm10_standard; + m->variant.air_quality_metrics.pm25_standard = data.pm25_standard; + m->variant.air_quality_metrics.pm100_standard = data.pm100_standard; + + m->variant.air_quality_metrics.pm10_environmental = data.pm10_env; + m->variant.air_quality_metrics.pm25_environmental = data.pm25_env; + m->variant.air_quality_metrics.pm100_environmental = data.pm100_env; + + LOG_INFO("(Sending): 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 replying 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("Sending packet to phone"); + service->sendToPhone(p); + } else { + LOG_INFO("Sending 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 new file mode 100644 index 0000000..fb8edd0 --- /dev/null +++ b/src/modules/Telemetry/AirQualityTelemetry.h @@ -0,0 +1,53 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#pragma once +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "Adafruit_PM25AQI.h" +#include "NodeDB.h" +#include "ProtobufModule.h" + +class AirQualityTelemetryModule : private concurrency::OSThread, public ProtobufModule +{ + CallbackObserver nodeStatusObserver = + CallbackObserver(this, + &AirQualityTelemetryModule::handleStatusUpdate); + + public: + AirQualityTelemetryModule() + : concurrency::OSThread("AirQualityTelemetryModule"), + ProtobufModule("AirQualityTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) + { + lastMeasurementPacket = nullptr; + setIntervalFromNow(10 * 1000); + aqi = Adafruit_PM25AQI(); + nodeStatusObserver.observe(&nodeStatus->onNewStatus); + } + + 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: + 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 new file mode 100644 index 0000000..4509334 --- /dev/null +++ b/src/modules/Telemetry/DeviceTelemetry.cpp @@ -0,0 +1,175 @@ +#include "DeviceTelemetry.h" +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "Default.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "PowerFSM.h" +#include "RTC.h" +#include "RadioLibInterface.h" +#include "Router.h" +#include "configuration.h" +#include "main.h" +#include +#include +#include + +#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_REPEATER && + config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN) { + 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; +} + +bool DeviceTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) +{ + // Don't worry about storing telemetry in NodeDB if we're a repeater + if (config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) + return false; + + if (t->which_variant == meshtastic_Telemetry_device_metrics_tag) { +#ifdef DEBUG_PORT + 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); +#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; + } + // Check for a request for device metrics + if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) { + LOG_INFO("Device telemetry replying to request"); + + meshtastic_Telemetry telemetry = getDeviceTelemetry(); + return allocDataProtobuf(telemetry); + } + } + 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(); +#if ARCH_PORTDUINO + t.variant.device_metrics.battery_level = MAGIC_USB_BATTERY_LEVEL; +#else + t.variant.device_metrics.battery_level = (!powerStatus->getHasBattery() || powerStatus->getIsCharging()) + ? MAGIC_USB_BATTERY_LEVEL + : powerStatus->getBatteryChargePercent(); +#endif + 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; +} + +void DeviceTelemetryModule::sendLocalStatsToPhone() +{ + 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; + } + 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); + + meshtastic_MeshPacket *p = allocDataProtobuf(telemetry); + 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("(Sending): 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); + + meshtastic_MeshPacket *p = allocDataProtobuf(telemetry); + 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("Sending packet to phone"); + service->sendToPhone(p); + } else { + LOG_INFO("Sending 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 new file mode 100644 index 0000000..6d7f698 --- /dev/null +++ b/src/modules/Telemetry/DeviceTelemetry.h @@ -0,0 +1,63 @@ +#pragma once +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "NodeDB.h" +#include "ProtobufModule.h" +#include +#include + +class DeviceTelemetryModule : private concurrency::OSThread, public ProtobufModule +{ + CallbackObserver nodeStatusObserver = + CallbackObserver(this, &DeviceTelemetryModule::handleStatusUpdate); + + public: + DeviceTelemetryModule() + : concurrency::OSThread("DeviceTelemetryModule"), + ProtobufModule("DeviceTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) + { + uptimeWrapCount = 0; + uptimeLastMs = millis(); + nodeStatusObserver.observe(&nodeStatus->onNewStatus); + setIntervalFromNow(45 * 1000); // 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); + + /** + * 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(); + 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++; + + uptimeLastMs = now; + } + + uint32_t uptimeWrapCount; + uint32_t uptimeLastMs; +}; diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp new file mode 100644 index 0000000..452c774 --- /dev/null +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -0,0 +1,589 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "Default.h" +#include "EnvironmentTelemetry.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "PowerFSM.h" +#include "RTC.h" +#include "Router.h" +#include "UnitConversions.h" +#include "main.h" +#include "power.h" +#include "sleep.h" +#include "target_specific.h" +#include +#include + +// Sensors +#include "Sensor/AHT10.h" +#include "Sensor/BME280Sensor.h" +#include "Sensor/BME680Sensor.h" +#include "Sensor/BMP085Sensor.h" +#include "Sensor/BMP280Sensor.h" +#include "Sensor/BMP3XXSensor.h" +#include "Sensor/DFRobotLarkSensor.h" +#include "Sensor/LPS22HBSensor.h" +#include "Sensor/MCP9808Sensor.h" +#include "Sensor/MLX90632Sensor.h" +#include "Sensor/NAU7802Sensor.h" +#include "Sensor/OPT3001Sensor.h" +#include "Sensor/RCWL9620Sensor.h" +#include "Sensor/SHT31Sensor.h" +#include "Sensor/SHT4XSensor.h" +#include "Sensor/SHTC3Sensor.h" +#include "Sensor/T1000xSensor.h" +#include "Sensor/TSL2591Sensor.h" +#include "Sensor/VEML7700Sensor.h" + +BMP085Sensor bmp085Sensor; +BMP280Sensor bmp280Sensor; +BME280Sensor bme280Sensor; +BME680Sensor bme680Sensor; +MCP9808Sensor mcp9808Sensor; +SHTC3Sensor shtc3Sensor; +LPS22HBSensor lps22hbSensor; +SHT31Sensor sht31Sensor; +VEML7700Sensor veml7700Sensor; +TSL2591Sensor tsl2591Sensor; +OPT3001Sensor opt3001Sensor; +SHT4XSensor sht4xSensor; +RCWL9620Sensor rcwl9620Sensor; +AHT10Sensor aht10Sensor; +MLX90632Sensor mlx90632Sensor; +DFRobotLarkSensor dfRobotLarkSensor; +NAU7802Sensor nau7802Sensor; +BMP3XXSensor bmp3xxSensor; +#ifdef T1000X_SENSOR_EN +T1000xSensor t1000xSensor; +#endif + +#define FAILED_STATE_SENSOR_READ_MULTIPLIER 10 +#define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true + +#include "graphics/ScreenFonts.h" +#include + +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("Sleeping for %ims, then awaking to send metrics again.", nightyNightMs); + doDeepSleep(nightyNightMs, true); + } + + 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; + + if (!(moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_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 = 0; + + if (moduleConfig.telemetry.environment_measurement_enabled) { + LOG_INFO("Environment Telemetry: Initializing"); + // 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 +#ifdef T1000X_SENSOR_EN + result = t1000xSensor.runOnce(); +#else + if (dfRobotLarkSensor.hasSensor()) + result = dfRobotLarkSensor.runOnce(); + if (bmp085Sensor.hasSensor()) + result = bmp085Sensor.runOnce(); + if (bmp280Sensor.hasSensor()) + result = bmp280Sensor.runOnce(); + if (bme280Sensor.hasSensor()) + result = bme280Sensor.runOnce(); + if (bmp3xxSensor.hasSensor()) + result = bmp3xxSensor.runOnce(); + if (bme680Sensor.hasSensor()) + result = bme680Sensor.runOnce(); + if (mcp9808Sensor.hasSensor()) + result = mcp9808Sensor.runOnce(); + if (shtc3Sensor.hasSensor()) + result = shtc3Sensor.runOnce(); + if (lps22hbSensor.hasSensor()) + result = lps22hbSensor.runOnce(); + if (sht31Sensor.hasSensor()) + result = sht31Sensor.runOnce(); + if (sht4xSensor.hasSensor()) + result = sht4xSensor.runOnce(); + if (ina219Sensor.hasSensor()) + result = ina219Sensor.runOnce(); + if (ina260Sensor.hasSensor()) + result = ina260Sensor.runOnce(); + if (ina3221Sensor.hasSensor()) + result = ina3221Sensor.runOnce(); + if (veml7700Sensor.hasSensor()) + result = veml7700Sensor.runOnce(); + if (tsl2591Sensor.hasSensor()) + result = tsl2591Sensor.runOnce(); + if (opt3001Sensor.hasSensor()) + result = opt3001Sensor.runOnce(); + if (rcwl9620Sensor.hasSensor()) + result = rcwl9620Sensor.runOnce(); + if (aht10Sensor.hasSensor()) + result = aht10Sensor.runOnce(); + if (mlx90632Sensor.hasSensor()) + result = mlx90632Sensor.runOnce(); + if (nau7802Sensor.hasSensor()) + result = nau7802Sensor.runOnce(); + if (max17048Sensor.hasSensor()) + result = max17048Sensor.runOnce(); +#endif + } + return result; + } 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) { + return disable(); + } else { + if (bme680Sensor.hasSensor()) + result = bme680Sensor.runTrigger(); + } + + 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; +} + +void EnvironmentTelemetryModule::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 "Environment" + display->drawString(x, y, "Environment"); + 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 "Env. From: ..." on its own + display->drawString(x, y, "Env. From: " + String(lastSender) + "(" + String(agoSecs) + "s)"); + + String last_temp = String(lastMeasurement.variant.environment_metrics.temperature, 0) + "°C"; + if (moduleConfig.telemetry.environment_display_fahrenheit) { + last_temp = + String(UnitConversions::CelsiusToFahrenheit(lastMeasurement.variant.environment_metrics.temperature), 0) + "°F"; + } + + // Continue with the remaining details + display->drawString(x, y += _fontHeight(FONT_SMALL), + "Temp/Hum: " + last_temp + " / " + + String(lastMeasurement.variant.environment_metrics.relative_humidity, 0) + "%"); + + if (lastMeasurement.variant.environment_metrics.barometric_pressure != 0) { + display->drawString(x, y += _fontHeight(FONT_SMALL), + "Press: " + String(lastMeasurement.variant.environment_metrics.barometric_pressure, 0) + "hPA"); + } + + if (lastMeasurement.variant.environment_metrics.voltage != 0) { + display->drawString(x, y += _fontHeight(FONT_SMALL), + "Volt/Cur: " + String(lastMeasurement.variant.environment_metrics.voltage, 0) + "V / " + + String(lastMeasurement.variant.environment_metrics.current, 0) + "mA"); + } + + if (lastMeasurement.variant.environment_metrics.iaq != 0) { + display->drawString(x, y += _fontHeight(FONT_SMALL), "IAQ: " + String(lastMeasurement.variant.environment_metrics.iaq)); + } + + if (lastMeasurement.variant.environment_metrics.distance != 0) + display->drawString(x, y += _fontHeight(FONT_SMALL), + "Water Level: " + String(lastMeasurement.variant.environment_metrics.distance, 0) + "mm"); + + if (lastMeasurement.variant.environment_metrics.weight != 0) + display->drawString(x, y += _fontHeight(FONT_SMALL), + "Weight: " + String(lastMeasurement.variant.environment_metrics.weight, 0) + "kg"); +} + +bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) +{ + if (t->which_variant == meshtastic_Telemetry_environment_metrics_tag) { +#ifdef DEBUG_PORT + 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", sender, t->variant.environment_metrics.voltage, + t->variant.environment_metrics.iaq, t->variant.environment_metrics.distance, t->variant.environment_metrics.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); + +#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 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; + +#ifdef T1000X_SENSOR_EN // add by WayenWeng + valid = valid && t1000xSensor.getMetrics(m); + hasSensor = true; +#else + if (dfRobotLarkSensor.hasSensor()) { + valid = valid && dfRobotLarkSensor.getMetrics(m); + hasSensor = true; + } + if (sht31Sensor.hasSensor()) { + valid = valid && sht31Sensor.getMetrics(m); + hasSensor = true; + } + if (sht4xSensor.hasSensor()) { + valid = valid && sht4xSensor.getMetrics(m); + hasSensor = true; + } + if (lps22hbSensor.hasSensor()) { + valid = valid && lps22hbSensor.getMetrics(m); + hasSensor = true; + } + if (shtc3Sensor.hasSensor()) { + valid = valid && shtc3Sensor.getMetrics(m); + hasSensor = true; + } + if (bmp085Sensor.hasSensor()) { + valid = valid && bmp085Sensor.getMetrics(m); + hasSensor = true; + } + if (bmp280Sensor.hasSensor()) { + valid = valid && bmp280Sensor.getMetrics(m); + hasSensor = true; + } + if (bme280Sensor.hasSensor()) { + valid = valid && bme280Sensor.getMetrics(m); + hasSensor = true; + } + if (bmp3xxSensor.hasSensor()) { + valid = valid && bmp3xxSensor.getMetrics(m); + hasSensor = true; + } + if (bme680Sensor.hasSensor()) { + valid = valid && bme680Sensor.getMetrics(m); + hasSensor = true; + } + if (mcp9808Sensor.hasSensor()) { + valid = valid && mcp9808Sensor.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 (veml7700Sensor.hasSensor()) { + valid = valid && veml7700Sensor.getMetrics(m); + hasSensor = true; + } + if (tsl2591Sensor.hasSensor()) { + valid = valid && tsl2591Sensor.getMetrics(m); + hasSensor = true; + } + if (opt3001Sensor.hasSensor()) { + valid = valid && opt3001Sensor.getMetrics(m); + hasSensor = true; + } + if (mlx90632Sensor.hasSensor()) { + valid = valid && mlx90632Sensor.getMetrics(m); + hasSensor = true; + } + if (rcwl9620Sensor.hasSensor()) { + valid = valid && rcwl9620Sensor.getMetrics(m); + hasSensor = true; + } + if (nau7802Sensor.hasSensor()) { + valid = valid && nau7802Sensor.getMetrics(m); + hasSensor = true; + } + if (aht10Sensor.hasSensor()) { + if (!bmp280Sensor.hasSensor() && !bmp3xxSensor.hasSensor()) { + valid = valid && aht10Sensor.getMetrics(m); + hasSensor = true; + } else if (bmp280Sensor.hasSensor()) { + // prefer bmp280 temp if both sensors are present, fetch only humidity + meshtastic_Telemetry m_ahtx = meshtastic_Telemetry_init_zero; + LOG_INFO("AHTX0+BMP280 module detected: using temp from BMP280 and humy from AHTX0"); + aht10Sensor.getMetrics(&m_ahtx); + m->variant.environment_metrics.relative_humidity = m_ahtx.variant.environment_metrics.relative_humidity; + } else { + // prefer bmp3xx temp if both sensors are present, fetch only humidity + meshtastic_Telemetry m_ahtx = meshtastic_Telemetry_init_zero; + LOG_INFO("AHTX0+BMP3XX module detected: using temp from BMP3XX and humy from AHTX0"); + aht10Sensor.getMetrics(&m_ahtx); + m->variant.environment_metrics.relative_humidity = m_ahtx.variant.environment_metrics.relative_humidity; + } + } + if (max17048Sensor.hasSensor()) { + valid = valid && max17048Sensor.getMetrics(m); + hasSensor = true; + } + +#endif + 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 replying to request"); + return allocDataProtobuf(m); + } else { + 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(); +#ifdef T1000X_SENSOR_EN + if (t1000xSensor.getMetrics(&m)) { +#else + if (getEnvironmentTelemetry(&m)) { +#endif + LOG_INFO("(Sending): 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("(Sending): 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("(Sending): 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); + + 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("Sending packet to phone"); + service->sendToPhone(p); + } else { + LOG_INFO("Sending 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("Starting next execution in 5 seconds and then going to sleep."); + sleepOnNextExecution = true; + setIntervalFromNow(5000); + } + } + return true; + } + return false; +} + +AdminMessageHandleResult EnvironmentTelemetryModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED; + if (dfRobotLarkSensor.hasSensor()) { + result = dfRobotLarkSensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + if (sht31Sensor.hasSensor()) { + result = sht31Sensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + if (lps22hbSensor.hasSensor()) { + result = lps22hbSensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + if (shtc3Sensor.hasSensor()) { + result = shtc3Sensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + if (bmp085Sensor.hasSensor()) { + result = bmp085Sensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + if (bmp280Sensor.hasSensor()) { + result = bmp280Sensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + if (bme280Sensor.hasSensor()) { + result = bme280Sensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + if (bmp3xxSensor.hasSensor()) { + result = bmp3xxSensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + if (bme680Sensor.hasSensor()) { + result = bme680Sensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + if (mcp9808Sensor.hasSensor()) { + result = mcp9808Sensor.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 (veml7700Sensor.hasSensor()) { + result = veml7700Sensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + if (tsl2591Sensor.hasSensor()) { + result = tsl2591Sensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + if (opt3001Sensor.hasSensor()) { + result = opt3001Sensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + if (mlx90632Sensor.hasSensor()) { + result = mlx90632Sensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + if (rcwl9620Sensor.hasSensor()) { + result = rcwl9620Sensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + if (nau7802Sensor.hasSensor()) { + result = nau7802Sensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + if (aht10Sensor.hasSensor()) { + result = aht10Sensor.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; + } + return result; +} + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/EnvironmentTelemetry.h b/src/modules/Telemetry/EnvironmentTelemetry.h new file mode 100644 index 0000000..e680d8b --- /dev/null +++ b/src/modules/Telemetry/EnvironmentTelemetry.h @@ -0,0 +1,63 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#pragma once +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "NodeDB.h" +#include "ProtobufModule.h" +#include +#include + +class EnvironmentTelemetryModule : private concurrency::OSThread, public ProtobufModule +{ + CallbackObserver nodeStatusObserver = + CallbackObserver(this, + &EnvironmentTelemetryModule::handleStatusUpdate); + + public: + EnvironmentTelemetryModule() + : concurrency::OSThread("EnvironmentTelemetryModule"), + 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); +#else + 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); + + virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; + + 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 new file mode 100644 index 0000000..9b86ae2 --- /dev/null +++ b/src/modules/Telemetry/HealthTelemetry.cpp @@ -0,0 +1,249 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_PORTDUINO) + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "Default.h" +#include "HealthTelemetry.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "PowerFSM.h" +#include "RTC.h" +#include "Router.h" +#include "UnitConversions.h" +#include "main.h" +#include "power.h" +#include "sleep.h" +#include "target_specific.h" +#include +#include + +// Sensors +#include "Sensor/MAX30102Sensor.h" +#include "Sensor/MLX90614Sensor.h" + +MAX30102Sensor max30102Sensor; +MLX90614Sensor mlx90614Sensor; + +#define FAILED_STATE_SENSOR_READ_MULTIPLIER 10 +#define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true + +#if (HAS_SCREEN) +#include "graphics/ScreenFonts.h" +#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("Sleeping for %ims, then awaking to send metrics again.", nightyNightMs); + doDeepSleep(nightyNightMs, true); + } + + 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: Initializing"); + // Initialize sensors + if (mlx90614Sensor.hasSensor()) + result = mlx90614Sensor.runOnce(); + if (max30102Sensor.hasSensor()) + result = max30102Sensor.runOnce(); + } + return result; + } 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 + display->drawString(x, y, "Health From: " + String(lastSender) + "(" + String(agoSecs) + "s)"); + + String last_temp = String(lastMeasurement.variant.health_metrics.temperature, 0) + "°C"; + if (moduleConfig.telemetry.environment_display_fahrenheit) { + last_temp = String(UnitConversions::CelsiusToFahrenheit(lastMeasurement.variant.health_metrics.temperature), 0) + "°F"; + } + + // Continue with the remaining details + display->drawString(x, y += _fontHeight(FONT_SMALL), "Temp: " + last_temp); + if (lastMeasurement.variant.health_metrics.has_heart_bpm) { + display->drawString(x, y += _fontHeight(FONT_SMALL), + "Heart Rate: " + String(lastMeasurement.variant.health_metrics.heart_bpm, 0) + " bpm"); + } + if (lastMeasurement.variant.health_metrics.has_spO2) { + display->drawString(x, y += _fontHeight(FONT_SMALL), + "spO2: " + String(lastMeasurement.variant.health_metrics.spO2, 0) + " %"); + } +} + +bool HealthTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) +{ + if (t->which_variant == meshtastic_Telemetry_health_metrics_tag) { +#ifdef DEBUG_PORT + 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); + +#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 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; + } + + 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 replying 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("(Sending): 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; + + 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("Sending packet to phone"); + service->sendToPhone(p); + } else { + LOG_INFO("Sending 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("Starting next execution in 5 seconds and then going to sleep."); + sleepOnNextExecution = true; + setIntervalFromNow(5000); + } + } + return true; + } + return false; +} + +#endif diff --git a/src/modules/Telemetry/HealthTelemetry.h b/src/modules/Telemetry/HealthTelemetry.h new file mode 100644 index 0000000..4ad0da8 --- /dev/null +++ b/src/modules/Telemetry/HealthTelemetry.h @@ -0,0 +1,60 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_PORTDUINO) + +#pragma once +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "NodeDB.h" +#include "ProtobufModule.h" +#include +#include + +class HealthTelemetryModule : private concurrency::OSThread, public ProtobufModule +{ + CallbackObserver nodeStatusObserver = + CallbackObserver(this, &HealthTelemetryModule::handleStatusUpdate); + + public: + HealthTelemetryModule() + : concurrency::OSThread("HealthTelemetryModule"), + 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); +#else + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; +#endif + + 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); + + 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 diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp new file mode 100644 index 0000000..c5f19b2 --- /dev/null +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -0,0 +1,256 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "Default.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "PowerFSM.h" +#include "PowerTelemetry.h" +#include "RTC.h" +#include "Router.h" +#include "main.h" +#include "power.h" +#include "sleep.h" +#include "target_specific.h" + +#define FAILED_STATE_SENSOR_READ_MULTIPLIER 10 +#define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true + +#include "graphics/ScreenFonts.h" +#include + +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("Sleeping for %ims, then awaking to send metrics again.", nightyNightMs); + doDeepSleep(nightyNightMs, true); + } + + 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.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 (firstTime) { + // This is the first time the OSThread library has called this function, so do some setup + firstTime = 0; +#if HAS_TELEMETRY && !defined(ARCH_PORTDUINO) + if (moduleConfig.telemetry.power_measurement_enabled) { + LOG_INFO("Power Telemetry: Initializing"); + // 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 + if (ina219Sensor.hasSensor() && !ina219Sensor.isInitialized()) + result = ina219Sensor.runOnce(); + if (ina260Sensor.hasSensor() && !ina260Sensor.isInitialized()) + result = ina260Sensor.runOnce(); + if (ina3221Sensor.hasSensor() && !ina3221Sensor.isInitialized()) + result = ina3221Sensor.runOnce(); + if (max17048Sensor.hasSensor() && !max17048Sensor.isInitialized()) + result = max17048Sensor.runOnce(); + } + return result; +#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, Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.power_update_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + 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 PowerTelemetryModule::wantUIFrame() +{ + return moduleConfig.telemetry.power_screen_enabled; +} + +void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_MEDIUM); + display->drawString(x, y, "Power Telemetry"); + if (lastMeasurementPacket == nullptr) { + display->setFont(FONT_SMALL); + display->drawString(x, y += _fontHeight(FONT_MEDIUM), "No measurement"); + return; + } + + 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->setFont(FONT_SMALL); + display->drawString(x, y += _fontHeight(FONT_MEDIUM), "Measurement Error"); + LOG_ERROR("Unable to decode last packet"); + return; + } + + // Display current and voltage based on ...power_metrics.has_[channel/voltage/current]... flags + display->setFont(FONT_SMALL); + display->drawString(x, y += _fontHeight(FONT_MEDIUM) - 2, "From: " + String(lastSender) + "(" + String(agoSecs) + "s)"); + if (lastMeasurement.variant.power_metrics.has_ch1_voltage || lastMeasurement.variant.power_metrics.has_ch1_current) { + display->drawString(x, y += _fontHeight(FONT_SMALL), + "Ch1 Volt: " + String(lastMeasurement.variant.power_metrics.ch1_voltage, 2) + + "V / Curr: " + String(lastMeasurement.variant.power_metrics.ch1_current, 0) + "mA"); + } + if (lastMeasurement.variant.power_metrics.has_ch2_voltage || lastMeasurement.variant.power_metrics.has_ch2_current) { + display->drawString(x, y += _fontHeight(FONT_SMALL), + "Ch2 Volt: " + String(lastMeasurement.variant.power_metrics.ch2_voltage, 2) + + "V / Curr: " + String(lastMeasurement.variant.power_metrics.ch2_current, 0) + "mA"); + } + if (lastMeasurement.variant.power_metrics.has_ch3_voltage || lastMeasurement.variant.power_metrics.has_ch3_current) { + display->drawString(x, y += _fontHeight(FONT_SMALL), + "Ch3 Volt: " + String(lastMeasurement.variant.power_metrics.ch3_voltage, 2) + + "V / Curr: " + String(lastMeasurement.variant.power_metrics.ch3_current, 0) + "mA"); + } +} + +bool PowerTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) +{ + if (t->which_variant == meshtastic_Telemetry_power_metrics_tag) { +#ifdef DEBUG_PORT + 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); +#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 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; +#if HAS_TELEMETRY && !defined(ARCH_PORTDUINO) + if (ina219Sensor.hasSensor()) + valid = ina219Sensor.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; +} + +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 replying to request"); + return allocDataProtobuf(m); + } else { + 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("(Sending): 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; + + 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("Sending packet to phone"); + service->sendToPhone(p); + } else { + LOG_INFO("Sending 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("Starting next execution in 5s then going to sleep."); + sleepOnNextExecution = true; + setIntervalFromNow(5000); + } + } + return true; + } + return false; +} + +#endif diff --git a/src/modules/Telemetry/PowerTelemetry.h b/src/modules/Telemetry/PowerTelemetry.h new file mode 100644 index 0000000..f824830 --- /dev/null +++ b/src/modules/Telemetry/PowerTelemetry.h @@ -0,0 +1,59 @@ +#pragma once + +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "NodeDB.h" +#include "ProtobufModule.h" +#include +#include + +class PowerTelemetryModule : private concurrency::OSThread, public ProtobufModule +{ + CallbackObserver nodeStatusObserver = + CallbackObserver(this, &PowerTelemetryModule::handleStatusUpdate); + + public: + PowerTelemetryModule() + : concurrency::OSThread("PowerTelemetryModule"), + 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); +#else + 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); + + 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 new file mode 100644 index 0000000..039b7da --- /dev/null +++ b/src/modules/Telemetry/Sensor/AHT10.cpp @@ -0,0 +1,44 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "AHT10.h" +#include "TelemetrySensor.h" + +#include +#include + +AHT10Sensor::AHT10Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_AHT10, "AHT10") {} + +int32_t AHT10Sensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + aht10 = Adafruit_AHTX0(); + status = aht10.begin(nodeTelemetrySensorsMap[sensorType].second, 0, nodeTelemetrySensorsMap[sensorType].first); + + return initI2CSensor(); +} + +void AHT10Sensor::setup() {} + +bool AHT10Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + LOG_DEBUG("AHT10Sensor::getMetrics"); + + sensors_event_t humidity, temp; + aht10.getEvent(&humidity, &temp); + + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_relative_humidity = true; + + 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/AHT10.h b/src/modules/Telemetry/Sensor/AHT10.h new file mode 100644 index 0000000..d9a1334 --- /dev/null +++ b/src/modules/Telemetry/Sensor/AHT10.h @@ -0,0 +1,23 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +class AHT10Sensor : public TelemetrySensor +{ + private: + Adafruit_AHTX0 aht10; + + protected: + virtual void setup() override; + + public: + AHT10Sensor(); + 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/BME280Sensor.cpp b/src/modules/Telemetry/Sensor/BME280Sensor.cpp new file mode 100644 index 0000000..9f5cf61 --- /dev/null +++ b/src/modules/Telemetry/Sensor/BME280Sensor.cpp @@ -0,0 +1,46 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "BME280Sensor.h" +#include "TelemetrySensor.h" +#include +#include + +BME280Sensor::BME280Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BME280, "BME280") {} + +int32_t BME280Sensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + status = bme280.begin(nodeTelemetrySensorsMap[sensorType].first, nodeTelemetrySensorsMap[sensorType].second); + + 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); + + return initI2CSensor(); +} + +void BME280Sensor::setup() {} + +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("BME280Sensor::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; +} +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/BME280Sensor.h b/src/modules/Telemetry/Sensor/BME280Sensor.h new file mode 100644 index 0000000..eb78f79 --- /dev/null +++ b/src/modules/Telemetry/Sensor/BME280Sensor.h @@ -0,0 +1,23 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +class BME280Sensor : public TelemetrySensor +{ + private: + Adafruit_BME280 bme280; + + protected: + virtual void setup() override; + + public: + BME280Sensor(); + 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/BME680Sensor.cpp b/src/modules/Telemetry/Sensor/BME680Sensor.cpp new file mode 100644 index 0000000..21c74c5 --- /dev/null +++ b/src/modules/Telemetry/Sensor/BME680Sensor.cpp @@ -0,0 +1,148 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "BME680Sensor.h" +#include "FSCommon.h" +#include "TelemetrySensor.h" + +BME680Sensor::BME680Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BME680, "BME680") {} + +int32_t BME680Sensor::runTrigger() +{ + if (!bme680.run()) { + checkStatus("runTrigger"); + } + return 35; +} + +int32_t BME680Sensor::runOnce() +{ + + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + if (!bme680.begin(nodeTelemetrySensorsMap[sensorType].first, *nodeTelemetrySensorsMap[sensorType].second)) + 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); + } else { + status = 0; + } + if (status == 0) + LOG_DEBUG("BME680Sensor::runOnce: bme680.status %d", bme680.status); + + return initI2CSensor(); +} + +void BME680Sensor::setup() {} + +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.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() +{ +#ifdef FSCom + 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); + } +#else + LOG_ERROR("ERROR: Filesystem not implemented"); +#endif +} + +void BME680Sensor::updateState() +{ +#ifdef FSCom + 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) { + 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); + } + } +#else + LOG_ERROR("ERROR: Filesystem not implemented"); +#endif +} + +void BME680Sensor::checkStatus(String functionName) +{ + if (bme680.status < BSEC_OK) + LOG_ERROR("%s BSEC2 code: %s", functionName.c_str(), String(bme680.status).c_str()); + else if (bme680.status > BSEC_OK) + LOG_WARN("%s BSEC2 code: %s", functionName.c_str(), String(bme680.status).c_str()); + + if (bme680.sensor.status < BME68X_OK) + LOG_ERROR("%s BME68X code: %s", functionName.c_str(), String(bme680.sensor.status).c_str()); + else if (bme680.sensor.status > BME68X_OK) + LOG_WARN("%s BME68X code: %s", functionName.c_str(), String(bme680.sensor.status).c_str()); +} + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/BME680Sensor.h b/src/modules/Telemetry/Sensor/BME680Sensor.h new file mode 100644 index 0000000..a5d2b5a --- /dev/null +++ b/src/modules/Telemetry/Sensor/BME680Sensor.h @@ -0,0 +1,46 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +#define STATE_SAVE_PERIOD UINT32_C(360 * 60 * 1000) // That's 6 hours worth of millis() + +const uint8_t bsec_config[] = { +#include "config/bme680/bme680_iaq_33v_3s_4d/bsec_iaq.txt" +}; + +class BME680Sensor : public TelemetrySensor +{ + private: + Bsec2 bme680; + + protected: + virtual void setup() override; + 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(String functionName); + + public: + BME680Sensor(); + int32_t runTrigger(); + 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/BMP085Sensor.cpp b/src/modules/Telemetry/Sensor/BMP085Sensor.cpp new file mode 100644 index 0000000..40ff996 --- /dev/null +++ b/src/modules/Telemetry/Sensor/BMP085Sensor.cpp @@ -0,0 +1,39 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "BMP085Sensor.h" +#include "TelemetrySensor.h" +#include +#include + +BMP085Sensor::BMP085Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BMP085, "BMP085") {} + +int32_t BMP085Sensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + bmp085 = Adafruit_BMP085(); + status = bmp085.begin(nodeTelemetrySensorsMap[sensorType].first, nodeTelemetrySensorsMap[sensorType].second); + + return initI2CSensor(); +} + +void BMP085Sensor::setup() {} + +bool BMP085Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_barometric_pressure = true; + + LOG_DEBUG("BMP085Sensor::getMetrics"); + measurement->variant.environment_metrics.temperature = bmp085.readTemperature(); + measurement->variant.environment_metrics.barometric_pressure = bmp085.readPressure() / 100.0F; + + 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 new file mode 100644 index 0000000..4ba8c5a --- /dev/null +++ b/src/modules/Telemetry/Sensor/BMP085Sensor.h @@ -0,0 +1,23 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +class BMP085Sensor : public TelemetrySensor +{ + private: + Adafruit_BMP085 bmp085; + + protected: + virtual void setup() override; + + public: + BMP085Sensor(); + 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/BMP280Sensor.cpp b/src/modules/Telemetry/Sensor/BMP280Sensor.cpp new file mode 100644 index 0000000..185e9b8 --- /dev/null +++ b/src/modules/Telemetry/Sensor/BMP280Sensor.cpp @@ -0,0 +1,45 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "BMP280Sensor.h" +#include "TelemetrySensor.h" +#include +#include + +BMP280Sensor::BMP280Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BMP280, "BMP280") {} + +int32_t BMP280Sensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + bmp280 = Adafruit_BMP280(nodeTelemetrySensorsMap[sensorType].second); + status = bmp280.begin(nodeTelemetrySensorsMap[sensorType].first); + + 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); + + return initI2CSensor(); +} + +void BMP280Sensor::setup() {} + +bool BMP280Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_barometric_pressure = true; + + LOG_DEBUG("BMP280Sensor::getMetrics"); + bmp280.takeForcedMeasurement(); + measurement->variant.environment_metrics.temperature = bmp280.readTemperature(); + measurement->variant.environment_metrics.barometric_pressure = bmp280.readPressure() / 100.0F; + + 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 new file mode 100644 index 0000000..da85fdc --- /dev/null +++ b/src/modules/Telemetry/Sensor/BMP280Sensor.h @@ -0,0 +1,23 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +class BMP280Sensor : public TelemetrySensor +{ + private: + Adafruit_BMP280 bmp280; + + protected: + virtual void setup() override; + + public: + BMP280Sensor(); + 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/BMP3XXSensor.cpp b/src/modules/Telemetry/Sensor/BMP3XXSensor.cpp new file mode 100644 index 0000000..4362396 --- /dev/null +++ b/src/modules/Telemetry/Sensor/BMP3XXSensor.cpp @@ -0,0 +1,89 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "BMP3XXSensor.h" + +BMP3XXSensor::BMP3XXSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BMP3XX, "BMP3XX") {} + +void BMP3XXSensor::setup() {} + +int32_t BMP3XXSensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + + // Get a singleton instance and initialise the bmp3xx + if (bmp3xx == nullptr) { + bmp3xx = BMP3XXSingleton::GetInstance(); + } + status = bmp3xx->begin_I2C(nodeTelemetrySensorsMap[sensorType].first, nodeTelemetrySensorsMap[sensorType].second); + + // 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(); + } + return initI2CSensor(); +} + +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.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("BMP3XXSensor::getMetrics id: %i temp: %.1f press %.1f", measurement->which_variant, + measurement->variant.environment_metrics.temperature, + measurement->variant.environment_metrics.barometric_pressure); + } else { + LOG_DEBUG("BMP3XXSensor::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() {} + +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; +} + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/BMP3XXSensor.h b/src/modules/Telemetry/Sensor/BMP3XXSensor.h new file mode 100644 index 0000000..79939c8 --- /dev/null +++ b/src/modules/Telemetry/Sensor/BMP3XXSensor.h @@ -0,0 +1,56 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#ifndef _BMP3XX_SENSOR_H +#define _BMP3XX_SENSOR_H + +#define SEAL_LEVEL_HPA 1013.2f + +#include "TelemetrySensor.h" +#include +#include + +// Singleton wrapper for the Adafruit_BMP3XX class +class BMP3XXSingleton : public Adafruit_BMP3XX +{ + private: + static BMP3XXSingleton *pinstance; + + protected: + BMP3XXSingleton(); + ~BMP3XXSingleton(); + + public: + // Create a singleton instance (not thread safe) + static BMP3XXSingleton *GetInstance(); + + // Singletons should not be cloneable. + BMP3XXSingleton(BMP3XXSingleton &other) = 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(); + + // Altitude in metres above mean sea level, assigned after calling performReading() + double altitudeAmslMetres = 0.0f; +}; + +class BMP3XXSensor : public TelemetrySensor +{ + protected: + BMP3XXSingleton *bmp3xx = nullptr; + virtual void setup() override; + + public: + BMP3XXSensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; +}; + +#endif + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/DFRobotLarkSensor.cpp b/src/modules/Telemetry/Sensor/DFRobotLarkSensor.cpp new file mode 100644 index 0000000..1d143b0 --- /dev/null +++ b/src/modules/Telemetry/Sensor/DFRobotLarkSensor.cpp @@ -0,0 +1,59 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "DFRobotLarkSensor.h" +#include "TelemetrySensor.h" +#include "gps/GeoCoord.h" +#include +#include + +DFRobotLarkSensor::DFRobotLarkSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_DFROBOT_LARK, "DFROBOT_LARK") {} + +int32_t DFRobotLarkSensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + + lark = DFRobot_LarkWeatherStation_I2C(nodeTelemetrySensorsMap[sensorType].first, nodeTelemetrySensorsMap[sensorType].second); + + if (lark.begin() == 0) // DFRobotLarkSensor init + { + LOG_DEBUG("DFRobotLarkSensor Init Succeed"); + status = true; + } else { + LOG_ERROR("DFRobotLarkSensor Init Failed"); + status = false; + } + return initI2CSensor(); +} + +void DFRobotLarkSensor::setup() {} + +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(); + + 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; +} + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/DFRobotLarkSensor.h b/src/modules/Telemetry/Sensor/DFRobotLarkSensor.h new file mode 100644 index 0000000..7a988e8 --- /dev/null +++ b/src/modules/Telemetry/Sensor/DFRobotLarkSensor.h @@ -0,0 +1,29 @@ +#pragma once + +#ifndef _MT_DFROBOTLARKSENSOR_H +#define _MT_DFROBOTLARKSENSOR_H +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include +#include + +class DFRobotLarkSensor : public TelemetrySensor +{ + private: + DFRobot_LarkWeatherStation_I2C lark = DFRobot_LarkWeatherStation_I2C(); + + protected: + virtual void setup() override; + + public: + DFRobotLarkSensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; +}; + +#endif +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/INA219Sensor.cpp b/src/modules/Telemetry/Sensor/INA219Sensor.cpp new file mode 100644 index 0000000..de69163 --- /dev/null +++ b/src/modules/Telemetry/Sensor/INA219Sensor.cpp @@ -0,0 +1,48 @@ +#include "configuration.h" + +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "INA219Sensor.h" +#include "TelemetrySensor.h" +#include + +#ifndef INA219_MULTIPLIER +#define INA219_MULTIPLIER 1.0f +#endif + +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(); +} + +void INA219Sensor::setup() {} + +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; +} + +uint16_t INA219Sensor::getBusVoltageMv() +{ + return lround(ina219.getBusVoltage_V() * 1000); +} + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/INA219Sensor.h b/src/modules/Telemetry/Sensor/INA219Sensor.h new file mode 100644 index 0000000..9dded06 --- /dev/null +++ b/src/modules/Telemetry/Sensor/INA219Sensor.h @@ -0,0 +1,25 @@ +#include "configuration.h" + +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include "VoltageSensor.h" +#include + +class INA219Sensor : public TelemetrySensor, VoltageSensor +{ + private: + Adafruit_INA219 ina219; + + protected: + virtual void setup() override; + + public: + INA219Sensor(); + 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/INA260Sensor.cpp b/src/modules/Telemetry/Sensor/INA260Sensor.cpp new file mode 100644 index 0000000..24182b3 --- /dev/null +++ b/src/modules/Telemetry/Sensor/INA260Sensor.cpp @@ -0,0 +1,43 @@ +#include "configuration.h" + +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "INA260Sensor.h" +#include "TelemetrySensor.h" +#include + +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; + } + + 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; + + // 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()); +} + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/INA260Sensor.h b/src/modules/Telemetry/Sensor/INA260Sensor.h new file mode 100644 index 0000000..f436b8f --- /dev/null +++ b/src/modules/Telemetry/Sensor/INA260Sensor.h @@ -0,0 +1,25 @@ +#include "configuration.h" + +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include "VoltageSensor.h" +#include + +class INA260Sensor : public TelemetrySensor, VoltageSensor +{ + private: + Adafruit_INA260 ina260 = Adafruit_INA260(); + + protected: + virtual void setup() 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 new file mode 100644 index 0000000..ed09856 --- /dev/null +++ b/src/modules/Telemetry/Sensor/INA3221Sensor.cpp @@ -0,0 +1,105 @@ +#include "configuration.h" + +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "INA3221Sensor.h" +#include "TelemetrySensor.h" +#include + +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(); +}; + +void INA3221Sensor::setup() {} + +struct _INA3221Measurement INA3221Sensor::getMeasurement(ina3221_ch_t ch) +{ + struct _INA3221Measurement measurement; + + measurement.voltage = ina3221.getVoltage(ch); + measurement.current = ina3221.getCurrent(ch); + + return measurement; +} + +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); + } + + return measurements; +} + +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); + } + + // unsupported metric + return false; +} + +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.voltage = m.voltage; + measurement->variant.environment_metrics.current = m.current; + + return true; +} + +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.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; +} + +uint16_t INA3221Sensor::getBusVoltageMv() +{ + return lround(ina3221.getVoltage(BAT_CH) * 1000); +} + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/INA3221Sensor.h b/src/modules/Telemetry/Sensor/INA3221Sensor.h new file mode 100644 index 0000000..d5121aa --- /dev/null +++ b/src/modules/Telemetry/Sensor/INA3221Sensor.h @@ -0,0 +1,50 @@ +#include "configuration.h" + +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include "VoltageSensor.h" +#include + +class INA3221Sensor : public TelemetrySensor, VoltageSensor +{ + private: + INA3221 ina3221 = INA3221(INA3221_ADDR42_SDA); + + // channel to report voltage/current for environment metrics + ina3221_ch_t ENV_CH = INA3221_CH1; + + // channel to report battery voltage for device_battery_ina_address + ina3221_ch_t BAT_CH = INA3221_CH1; + + // get a single measurement for a channel + struct _INA3221Measurement getMeasurement(ina3221_ch_t ch); + + // get all measurements for all channels + struct _INA3221Measurements getMeasurements(); + + bool getEnvironmentMetrics(meshtastic_Telemetry *measurement); + bool getPowerMetrics(meshtastic_Telemetry *measurement); + + protected: + void setup() override; + + public: + INA3221Sensor(); + int32_t runOnce() override; + bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual uint16_t getBusVoltageMv() override; +}; + +struct _INA3221Measurement { + float voltage; + float current; +}; + +struct _INA3221Measurements { + // INA3221 has 3 channels + struct _INA3221Measurement measurements[3]; +}; + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/LPS22HBSensor.cpp b/src/modules/Telemetry/Sensor/LPS22HBSensor.cpp new file mode 100644 index 0000000..170fafd --- /dev/null +++ b/src/modules/Telemetry/Sensor/LPS22HBSensor.cpp @@ -0,0 +1,43 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "LPS22HBSensor.h" +#include "TelemetrySensor.h" +#include +#include + +LPS22HBSensor::LPS22HBSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_LPS22, "LPS22HB") {} + +int32_t LPS22HBSensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + status = lps22hb.begin_I2C(nodeTelemetrySensorsMap[sensorType].first, nodeTelemetrySensorsMap[sensorType].second); + return initI2CSensor(); +} + +void LPS22HBSensor::setup() +{ + lps22hb.setDataRate(LPS22_RATE_10_HZ); +} + +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); + + measurement->variant.environment_metrics.temperature = temp.temperature; + measurement->variant.environment_metrics.barometric_pressure = pressure.pressure; + + 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 new file mode 100644 index 0000000..955f2a1 --- /dev/null +++ b/src/modules/Telemetry/Sensor/LPS22HBSensor.h @@ -0,0 +1,24 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include +#include + +class LPS22HBSensor : public TelemetrySensor +{ + private: + Adafruit_LPS22 lps22hb; + + protected: + virtual void setup() override; + + public: + LPS22HBSensor(); + 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/MAX17048Sensor.cpp b/src/modules/Telemetry/Sensor/MAX17048Sensor.cpp new file mode 100644 index 0000000..02ab9df --- /dev/null +++ b/src/modules/Telemetry/Sensor/MAX17048Sensor.cpp @@ -0,0 +1,176 @@ +#include "MAX17048Sensor.h" + +#if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) + +MAX17048Singleton *MAX17048Singleton::GetInstance() +{ + if (pinstance == nullptr) { + pinstance = new MAX17048Singleton(); + } + return pinstance; +} + +MAX17048Singleton::MAX17048Singleton() {} + +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::isBatteryCharging() +{ + float volts = cellVoltage(); + if (isnan(volts)) { + LOG_DEBUG("%s::isBatteryCharging is 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 + + // 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; + } + + 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); +} + +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; +} + +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); + 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; + } + + 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(); +} + +void MAX17048Sensor::setup() {} + +bool MAX17048Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + LOG_DEBUG("MAX17048Sensor::getMetrics id: %i", measurement->which_variant); + + float volts = max17048->cellVoltage(); + if (isnan(volts)) { + LOG_DEBUG("MAX17048Sensor::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 + + LOG_DEBUG("MAX17048Sensor::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(); +}; + +#endif + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/MAX17048Sensor.h b/src/modules/Telemetry/Sensor/MAX17048Sensor.h new file mode 100644 index 0000000..bd109cb --- /dev/null +++ b/src/modules/Telemetry/Sensor/MAX17048Sensor.h @@ -0,0 +1,111 @@ +#pragma once + +#ifndef MAX17048_SENSOR_H +#define MAX17048_SENSOR_H + +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) + +// Samples to store in a buffer to determine if the battery is charging or discharging +#define MAX17048_CHARGING_SAMPLES 3 + +// Threshold to determine if the battery is on charge, in percent/hour +#define MAX17048_CHARGING_MINIMUM_RATE 1.0f + +// Threshold to determine if the board has bus power +#define MAX17048_BUS_POWER_VOLTS 4.195f + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include "VoltageSensor.h" + +#include "meshUtils.h" +#include +#include + +struct MAX17048ChargeSample { + 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"; + + protected: + MAX17048Singleton(); + ~MAX17048Singleton(); + + public: + // Create a singleton instance (not thread safe) + static MAX17048Singleton *GetInstance(); + + // Singletons should not be cloneable. + MAX17048Singleton(MAX17048Singleton &other) = delete; + + // Singletons should not be assignable. + void operator=(const MAX17048Singleton &) = delete; + + // Initialise the sensor (not thread safe) + virtual bool runOnce(TwoWire *theWire = &Wire); + + // Get the current bus voltage + uint16_t getBusVoltageMv(); + + // Get the state of charge in percent 0 to 100 + uint8_t getBusBatteryPercent(); + + // 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 is currently on charge (not thread safe) + bool isBatteryCharging(); + + // Returns true if a battery is actually connected + bool isBatteryConnected(); + + // 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; + + protected: + virtual void setup() override; + + public: + MAX17048Sensor(); + + // 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 + virtual uint16_t getBusVoltageMv() override; +}; + +#endif + +#endif + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/MAX30102Sensor.cpp b/src/modules/Telemetry/Sensor/MAX30102Sensor.cpp new file mode 100644 index 0000000..88128a6 --- /dev/null +++ b/src/modules/Telemetry/Sensor/MAX30102Sensor.cpp @@ -0,0 +1,83 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_PORTDUINO) + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "MAX30102Sensor.h" +#include "TelemetrySensor.h" +#include + +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; + } + + 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(); +} + +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(); + + 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(); + } + + 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 diff --git a/src/modules/Telemetry/Sensor/MAX30102Sensor.h b/src/modules/Telemetry/Sensor/MAX30102Sensor.h new file mode 100644 index 0000000..426d9d3 --- /dev/null +++ b/src/modules/Telemetry/Sensor/MAX30102Sensor.h @@ -0,0 +1,26 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_PORTDUINO) + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +#define MAX30102_BUFFER_LEN 100 + +class MAX30102Sensor : public TelemetrySensor +{ + private: + MAX30105 max30102 = MAX30105(); + uint32_t _speed = 200000UL; + + protected: + virtual void setup() override; + + public: + MAX30102Sensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; +}; + +#endif diff --git a/src/modules/Telemetry/Sensor/MCP9808Sensor.cpp b/src/modules/Telemetry/Sensor/MCP9808Sensor.cpp new file mode 100644 index 0000000..6271076 --- /dev/null +++ b/src/modules/Telemetry/Sensor/MCP9808Sensor.cpp @@ -0,0 +1,36 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "MCP9808Sensor.h" +#include "TelemetrySensor.h" +#include + +MCP9808Sensor::MCP9808Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MCP9808, "MCP9808") {} + +int32_t MCP9808Sensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + status = mcp9808.begin(nodeTelemetrySensorsMap[sensorType].first, nodeTelemetrySensorsMap[sensorType].second); + return initI2CSensor(); +} + +void MCP9808Sensor::setup() +{ + mcp9808.setResolution(2); +} + +bool MCP9808Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_temperature = true; + + LOG_DEBUG("MCP9808Sensor::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 new file mode 100644 index 0000000..05bdabf --- /dev/null +++ b/src/modules/Telemetry/Sensor/MCP9808Sensor.h @@ -0,0 +1,23 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +class MCP9808Sensor : public TelemetrySensor +{ + private: + Adafruit_MCP9808 mcp9808; + + protected: + virtual void setup() override; + + public: + MCP9808Sensor(); + 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/MLX90614Sensor.cpp b/src/modules/Telemetry/Sensor/MLX90614Sensor.cpp new file mode 100644 index 0000000..3a13eeb --- /dev/null +++ b/src/modules/Telemetry/Sensor/MLX90614Sensor.cpp @@ -0,0 +1,44 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_PORTDUINO) + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "MLX90614Sensor.h" +#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."); + } + 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; +} + +#endif diff --git a/src/modules/Telemetry/Sensor/MLX90614Sensor.h b/src/modules/Telemetry/Sensor/MLX90614Sensor.h new file mode 100644 index 0000000..00f6344 --- /dev/null +++ b/src/modules/Telemetry/Sensor/MLX90614Sensor.h @@ -0,0 +1,24 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_PORTDUINO) +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +#define MLX90614_EMISSIVITY 0.98 // human skin + +class MLX90614Sensor : public TelemetrySensor +{ + private: + Adafruit_MLX90614 mlx = Adafruit_MLX90614(); + + protected: + virtual void setup() 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 new file mode 100644 index 0000000..b7bd6ae --- /dev/null +++ b/src/modules/Telemetry/Sensor/MLX90632Sensor.cpp @@ -0,0 +1,41 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "MLX90632Sensor.h" +#include "TelemetrySensor.h" + +MLX90632Sensor::MLX90632Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MLX90632, "MLX90632") {} + +int32_t MLX90632Sensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + + MLX90632::status returnError; + if (mlx.begin(nodeTelemetrySensorsMap[sensorType].first, *nodeTelemetrySensorsMap[sensorType].second, returnError) == + true) // MLX90632 init + { + LOG_DEBUG("MLX90632 Init Succeed"); + status = true; + } else { + LOG_ERROR("MLX90632 Init Failed"); + status = false; + } + return initI2CSensor(); +} + +void MLX90632Sensor::setup() {} + +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; +} + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/MLX90632Sensor.h b/src/modules/Telemetry/Sensor/MLX90632Sensor.h new file mode 100644 index 0000000..7b36c44 --- /dev/null +++ b/src/modules/Telemetry/Sensor/MLX90632Sensor.h @@ -0,0 +1,23 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +class MLX90632Sensor : public TelemetrySensor +{ + private: + MLX90632 mlx = MLX90632(); + + protected: + virtual void setup() override; + + public: + MLX90632Sensor(); + 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/NAU7802Sensor.cpp b/src/modules/Telemetry/Sensor/NAU7802Sensor.cpp new file mode 100644 index 0000000..856a1ae --- /dev/null +++ b/src/modules/Telemetry/Sensor/NAU7802Sensor.cpp @@ -0,0 +1,140 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "FSCommon.h" +#include "NAU7802Sensor.h" +#include "SafeFile.h" +#include "TelemetrySensor.h" +#include +#include +#include + +meshtastic_Nau7802Config nau7802config = meshtastic_Nau7802Config_init_zero; + +NAU7802Sensor::NAU7802Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_NAU7802, "NAU7802") {} + +int32_t NAU7802Sensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + status = nau7802.begin(*nodeTelemetrySensorsMap[sensorType].second); + 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()); + return initI2CSensor(); +} + +void NAU7802Sensor::setup() {} + +bool NAU7802Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + LOG_DEBUG("NAU7802Sensor::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; + + 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)); + } else { + okay = true; + } + okay &= file.close(); + + return okay; +} + +bool NAU7802Sensor::loadCalibrationData() +{ + 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); + } + 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 new file mode 100644 index 0000000..c53a3b3 --- /dev/null +++ b/src/modules/Telemetry/Sensor/NAU7802Sensor.h @@ -0,0 +1,31 @@ +#include "MeshModule.h" +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +class NAU7802Sensor : public TelemetrySensor +{ + private: + NAU7802 nau7802; + + protected: + virtual void setup() override; + const char *nau7802ConfigFileName = "/prefs/nau7802.dat"; + bool saveCalibrationData(); + bool loadCalibrationData(); + + public: + NAU7802Sensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) 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 new file mode 100644 index 0000000..75c6cd4 --- /dev/null +++ b/src/modules/Telemetry/Sensor/OPT3001Sensor.cpp @@ -0,0 +1,50 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "OPT3001Sensor.h" +#include "TelemetrySensor.h" +#include + +OPT3001Sensor::OPT3001Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_OPT3001, "OPT3001") {} + +int32_t OPT3001Sensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + auto errorCode = opt3001.begin(nodeTelemetrySensorsMap[sensorType].first); + status = errorCode == NO_ERROR; + + return initI2CSensor(); +} + +void OPT3001Sensor::setup() +{ + 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); + } +} + +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); + + 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 new file mode 100644 index 0000000..2ac1493 --- /dev/null +++ b/src/modules/Telemetry/Sensor/OPT3001Sensor.h @@ -0,0 +1,24 @@ +#pragma once +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +class OPT3001Sensor : public TelemetrySensor +{ + private: + ClosedCube_OPT3001 opt3001; + + protected: + virtual void setup() override; + + public: + OPT3001Sensor(); + 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/RCWL9620Sensor.cpp b/src/modules/Telemetry/Sensor/RCWL9620Sensor.cpp new file mode 100644 index 0000000..c7421d2 --- /dev/null +++ b/src/modules/Telemetry/Sensor/RCWL9620Sensor.cpp @@ -0,0 +1,66 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "RCWL9620Sensor.h" +#include "TelemetrySensor.h" + +RCWL9620Sensor::RCWL9620Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_RCWL9620, "RCWL9620") {} + +int32_t RCWL9620Sensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + status = 1; + begin(nodeTelemetrySensorsMap[sensorType].second, nodeTelemetrySensorsMap[sensorType].first); + return initI2CSensor(); +} + +void RCWL9620Sensor::setup() {} + +bool RCWL9620Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_distance = true; + LOG_DEBUG("RCWL9620Sensor::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(); +} + +float RCWL9620Sensor::getDistance() +{ + uint32_t data; + _wire->beginTransmission(_addr); // Transfer data to addr. + _wire->write(0x01); + _wire->endTransmission(); // Stop data transmission with the Ultrasonic + // Unit. + + _wire->requestFrom(_addr, + (uint8_t)3); // Request 3 bytes from Ultrasonic Unit. + + data = _wire->read(); + data <<= 8; + data |= _wire->read(); + data <<= 8; + data |= _wire->read(); + float Distance = float(data) / 1000; + if (Distance > 4500.00) { + return 4500.00; + } else { + return Distance; + } +} + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/RCWL9620Sensor.h b/src/modules/Telemetry/Sensor/RCWL9620Sensor.h new file mode 100644 index 0000000..7f9486d --- /dev/null +++ b/src/modules/Telemetry/Sensor/RCWL9620Sensor.h @@ -0,0 +1,29 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#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; + + protected: + virtual void setup() override; + 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 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/SHT31Sensor.cpp b/src/modules/Telemetry/Sensor/SHT31Sensor.cpp new file mode 100644 index 0000000..b96b94f --- /dev/null +++ b/src/modules/Telemetry/Sensor/SHT31Sensor.cpp @@ -0,0 +1,38 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "SHT31Sensor.h" +#include "TelemetrySensor.h" +#include + +SHT31Sensor::SHT31Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHT31, "SHT31") {} + +int32_t SHT31Sensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + sht31 = Adafruit_SHT31(nodeTelemetrySensorsMap[sensorType].second); + status = sht31.begin(nodeTelemetrySensorsMap[sensorType].first); + return initI2CSensor(); +} + +void SHT31Sensor::setup() +{ + // Set up oversampling and filter initialization +} + +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; +} + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHT31Sensor.h b/src/modules/Telemetry/Sensor/SHT31Sensor.h new file mode 100644 index 0000000..560b224 --- /dev/null +++ b/src/modules/Telemetry/Sensor/SHT31Sensor.h @@ -0,0 +1,23 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +class SHT31Sensor : public TelemetrySensor +{ + private: + Adafruit_SHT31 sht31; + + protected: + virtual void setup() override; + + public: + SHT31Sensor(); + 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/SHT4XSensor.cpp b/src/modules/Telemetry/Sensor/SHT4XSensor.cpp new file mode 100644 index 0000000..0fa6021 --- /dev/null +++ b/src/modules/Telemetry/Sensor/SHT4XSensor.cpp @@ -0,0 +1,52 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "SHT4XSensor.h" +#include "TelemetrySensor.h" +#include + +SHT4XSensor::SHT4XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHT4X, "SHT4X") {} + +int32_t SHT4XSensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + + uint32_t serialNumber = 0; + + sht4x.begin(nodeTelemetrySensorsMap[sensorType].second); + + serialNumber = sht4x.readSerial(); + if (serialNumber != 0) { + LOG_DEBUG("serialNumber : %x", serialNumber); + status = 1; + } else { + LOG_DEBUG("Error trying to execute readSerial(): "); + status = 0; + } + + return initI2CSensor(); +} + +void SHT4XSensor::setup() +{ + // Set up oversampling and filter initialization +} + +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; +} + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHT4XSensor.h b/src/modules/Telemetry/Sensor/SHT4XSensor.h new file mode 100644 index 0000000..62a5cef --- /dev/null +++ b/src/modules/Telemetry/Sensor/SHT4XSensor.h @@ -0,0 +1,23 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +class SHT4XSensor : public TelemetrySensor +{ + private: + Adafruit_SHT4x sht4x = Adafruit_SHT4x(); + + protected: + virtual void setup() override; + + public: + SHT4XSensor(); + 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/SHTC3Sensor.cpp b/src/modules/Telemetry/Sensor/SHTC3Sensor.cpp new file mode 100644 index 0000000..3a7cc48 --- /dev/null +++ b/src/modules/Telemetry/Sensor/SHTC3Sensor.cpp @@ -0,0 +1,41 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "SHTC3Sensor.h" +#include "TelemetrySensor.h" +#include + +SHTC3Sensor::SHTC3Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHTC3, "SHTC3") {} + +int32_t SHTC3Sensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + status = shtc3.begin(); + return initI2CSensor(); +} + +void SHTC3Sensor::setup() +{ + // Set up oversampling and filter initialization +} + +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); + + 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/SHTC3Sensor.h b/src/modules/Telemetry/Sensor/SHTC3Sensor.h new file mode 100644 index 0000000..7a76029 --- /dev/null +++ b/src/modules/Telemetry/Sensor/SHTC3Sensor.h @@ -0,0 +1,23 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +class SHTC3Sensor : public TelemetrySensor +{ + private: + Adafruit_SHTC3 shtc3 = Adafruit_SHTC3(); + + protected: + virtual void setup() override; + + public: + SHTC3Sensor(); + 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/T1000xSensor.cpp b/src/modules/Telemetry/Sensor/T1000xSensor.cpp new file mode 100644 index 0000000..068969e --- /dev/null +++ b/src/modules/Telemetry/Sensor/T1000xSensor.cpp @@ -0,0 +1,119 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(T1000X_SENSOR_EN) + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "T1000xSensor.h" +#include "TelemetrySensor.h" +#include + +#define T1000X_SENSE_SAMPLES 15 +#define T1000X_LIGHT_REF_VCC 2400 + +#define HEATER_NTC_BX 4250 // thermistor coefficient B +#define HEATER_NTC_RP 8250 // ohm, series resistance to thermistor +#define HEATER_NTC_KA 273.15 // 25 Celsius at Kelvin +#define NTC_REF_VCC 3000 // mV, output voltage of LDO + +// 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, +}; + +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, +}; + +T1000xSensor::T1000xSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SENSOR_UNSET, "T1000x") {} + +int32_t T1000xSensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; +} + +void T1000xSensor::setup() +{ + // Set up oversampling and filter initialization +} + +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; + } + } + 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 new file mode 100644 index 0000000..a1c771c --- /dev/null +++ b/src/modules/Telemetry/Sensor/T1000xSensor.h @@ -0,0 +1,21 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" + +class T1000xSensor : public TelemetrySensor +{ + protected: + virtual void setup() override; + + public: + T1000xSensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual float getLux(); + virtual float getTemp(); +}; + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/TSL2591Sensor.cpp b/src/modules/Telemetry/Sensor/TSL2591Sensor.cpp new file mode 100644 index 0000000..add475d --- /dev/null +++ b/src/modules/Telemetry/Sensor/TSL2591Sensor.cpp @@ -0,0 +1,44 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TSL2591Sensor.h" +#include "TelemetrySensor.h" +#include +#include + +TSL2591Sensor::TSL2591Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_TSL25911FN, "TSL2591") {} + +int32_t TSL2591Sensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + status = tsl.begin(nodeTelemetrySensorsMap[sensorType].second); + + return initI2CSensor(); +} + +void TSL2591Sensor::setup() +{ + tsl.setGain(TSL2591_GAIN_MED); // 25x gain + tsl.setTiming(TSL2591_INTEGRATIONTIME_300MS); +} + +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); + + return true; +} + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/TSL2591Sensor.h b/src/modules/Telemetry/Sensor/TSL2591Sensor.h new file mode 100644 index 0000000..27bebdf --- /dev/null +++ b/src/modules/Telemetry/Sensor/TSL2591Sensor.h @@ -0,0 +1,22 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +class TSL2591Sensor : public TelemetrySensor +{ + private: + Adafruit_TSL2591 tsl; + + protected: + virtual void setup() override; + + public: + TSL2591Sensor(); + 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/TelemetrySensor.cpp b/src/modules/Telemetry/Sensor/TelemetrySensor.cpp new file mode 100644 index 0000000..d6e7d1f --- /dev/null +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.cpp @@ -0,0 +1,10 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "NodeDB.h" +#include "TelemetrySensor.h" +#include "main.h" + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.h b/src/modules/Telemetry/Sensor/TelemetrySensor.h new file mode 100644 index 0000000..7910568 --- /dev/null +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.h @@ -0,0 +1,61 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#pragma once +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "MeshModule.h" +#include "NodeDB.h" +#include + +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("Could not connect to detected %s sensor. Removing 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 void setup(); + + public: + virtual AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) + { + return AdminMessageHandleResult::NOT_HANDLED; + } + + bool hasSensor() { return nodeTelemetrySensorsMap[sensorType].first > 0; } + + virtual int32_t runOnce() = 0; + virtual bool isInitialized() { return initialized; } + virtual bool isRunning() { return status > 0; } + + virtual bool getMetrics(meshtastic_Telemetry *measurement) = 0; +}; + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/VEML7700Sensor.cpp b/src/modules/Telemetry/Sensor/VEML7700Sensor.cpp new file mode 100644 index 0000000..496b49a --- /dev/null +++ b/src/modules/Telemetry/Sensor/VEML7700Sensor.cpp @@ -0,0 +1,68 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include "VEML7700Sensor.h" + +#include +#include + +VEML7700Sensor::VEML7700Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_VEML7700, "VEML7700") {} + +int32_t VEML7700Sensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + status = veml7700.begin(nodeTelemetrySensorsMap[sensorType].second); + + veml7700.setLowThreshold(10000); + veml7700.setHighThreshold(20000); + veml7700.interruptEnable(true); + + return initI2CSensor(); +} + +void VEML7700Sensor::setup() {} + +/*! + * @brief Copmute lux from ALS reading. + * @param rawALS raw ALS register value + * @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; +} + +/*! + * @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()); +} + +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); + + 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 new file mode 100644 index 0000000..97e5733 --- /dev/null +++ b/src/modules/Telemetry/Sensor/VEML7700Sensor.h @@ -0,0 +1,27 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#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); + + protected: + virtual void setup() override; + + public: + VEML7700Sensor(); + 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/VoltageSensor.h b/src/modules/Telemetry/Sensor/VoltageSensor.h new file mode 100644 index 0000000..767ffd2 --- /dev/null +++ b/src/modules/Telemetry/Sensor/VoltageSensor.h @@ -0,0 +1,13 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#pragma once + +class VoltageSensor +{ + public: + virtual uint16_t getBusVoltageMv() = 0; +}; + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/UnitConversions.cpp b/src/modules/Telemetry/UnitConversions.cpp new file mode 100644 index 0000000..9f40de4 --- /dev/null +++ b/src/modules/Telemetry/UnitConversions.cpp @@ -0,0 +1,21 @@ +#include "UnitConversions.h" + +float UnitConversions::CelsiusToFahrenheit(float celcius) +{ + return (celcius * 9) / 5 + 32; +} + +float UnitConversions::MetersPerSecondToKnots(float metersPerSecond) +{ + return metersPerSecond * 1.94384; +} + +float UnitConversions::MetersPerSecondToMilesPerHour(float metersPerSecond) +{ + return metersPerSecond * 2.23694; +} + +float UnitConversions::HectoPascalToInchesOfMercury(float hectoPascal) +{ + return hectoPascal * 0.029529983071445; +} diff --git a/src/modules/Telemetry/UnitConversions.h b/src/modules/Telemetry/UnitConversions.h new file mode 100644 index 0000000..60f9b66 --- /dev/null +++ b/src/modules/Telemetry/UnitConversions.h @@ -0,0 +1,10 @@ +#pragma once + +class UnitConversions +{ + public: + static float CelsiusToFahrenheit(float celcius); + 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 new file mode 100644 index 0000000..f1d01ad --- /dev/null +++ b/src/modules/TextMessageModule.cpp @@ -0,0 +1,29 @@ +#include "TextMessageModule.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "PowerFSM.h" +#include "buzz.h" +#include "configuration.h" +TextMessageModule *textMessageModule; + +ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp) +{ +#ifdef DEBUG_PORT + 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. + // Keep a copy of the most recent text message. + devicestate.rx_text_message = mp; + devicestate.has_rx_text_message = true; + + powerFSM.trigger(EVENT_RECEIVED_MSG); + notifyObservers(&mp); + + return ProcessMessage::CONTINUE; // Let others look at this message also if they want +} + +bool TextMessageModule::wantPacket(const meshtastic_MeshPacket *p) +{ + return MeshService::isTextPayload(p); +} \ No newline at end of file diff --git a/src/modules/TextMessageModule.h b/src/modules/TextMessageModule.h new file mode 100644 index 0000000..cc0b0f9 --- /dev/null +++ b/src/modules/TextMessageModule.h @@ -0,0 +1,26 @@ +#pragma once +#include "Observer.h" +#include "SinglePortModule.h" + +/** + * Text message handling for meshtastic - draws on the OLED display the most recent received message + */ +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; +}; + +extern TextMessageModule *textMessageModule; \ No newline at end of file diff --git a/src/modules/TraceRouteModule.cpp b/src/modules/TraceRouteModule.cpp new file mode 100644 index 0000000..79b14de --- /dev/null +++ b/src/modules/TraceRouteModule.cpp @@ -0,0 +1,173 @@ +#include "TraceRouteModule.h" +#include "MeshService.h" +#include "meshUtils.h" + +TraceRouteModule *traceRouteModule; + +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; + + // 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); +} + +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 + if (p.hop_start != 0 && p.hop_limit <= p.hop_start) { + uint8_t hopsTaken = p.hop_start - p.hop_limit; + int8_t diff = hopsTaken - *route_count; + for (uint8_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 (uint8_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) +{ +#ifdef DEBUG_PORT + std::string route = "Route traced:"; + 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()) { + 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); + + // 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); + + return reply; +} + +TraceRouteModule::TraceRouteModule() + : ProtobufModule("traceroute", meshtastic_PortNum_TRACEROUTE_APP, &meshtastic_RouteDiscovery_msg) +{ + ourPortNum = meshtastic_PortNum_TRACEROUTE_APP; + isPromiscuous = true; // We need to update the route even if it is not destined to us +} \ No newline at end of file diff --git a/src/modules/TraceRouteModule.h b/src/modules/TraceRouteModule.h new file mode 100644 index 0000000..afe2b38 --- /dev/null +++ b/src/modules/TraceRouteModule.h @@ -0,0 +1,36 @@ +#pragma once +#include "ProtobufModule.h" + +#define ROUTE_SIZE sizeof(((meshtastic_RouteDiscovery *)0)->route) / sizeof(((meshtastic_RouteDiscovery *)0)->route[0]) + +/** + * A module that traces the route to a certain destination node + */ +class TraceRouteModule : public ProtobufModule +{ + public: + TraceRouteModule(); + + protected: + bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_RouteDiscovery *r) 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; + + private: + // 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 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); +}; + +extern TraceRouteModule *traceRouteModule; \ No newline at end of file diff --git a/src/modules/WaypointModule.cpp b/src/modules/WaypointModule.cpp new file mode 100644 index 0000000..48e0c42 --- /dev/null +++ b/src/modules/WaypointModule.cpp @@ -0,0 +1,186 @@ +#include "WaypointModule.h" +#include "NodeDB.h" +#include "PowerFSM.h" +#include "configuration.h" +#if HAS_SCREEN +#include "gps/RTC.h" +#include "graphics/Screen.h" +#include "main.h" +#endif + +WaypointModule *waypointModule; + +ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp) +{ +#ifdef DEBUG_PORT + 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; + + powerFSM.trigger(EVENT_RECEIVED_MSG); + +#if HAS_SCREEN + + UIFrameEvent e; + + // 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; + + notifyObservers(&e); + +#endif + + return ProcessMessage::CONTINUE; // Let others look at this message also if they want +} + +#if HAS_SCREEN +bool WaypointModule::shouldDraw() +{ +#if !MESHTASTIC_EXCLUDE_WAYPOINT + // If no waypoint to show + if (!devicestate.has_rx_waypoint) + return false; + + // Decode the message, to find the expiration time (is waypoint still valid) + // This handles "deletion" as well as expiration + meshtastic_Waypoint wp; + memset(&wp, 0, sizeof(wp)); + if (pb_decode_from_bytes(devicestate.rx_waypoint.decoded.payload.bytes, devicestate.rx_waypoint.decoded.payload.size, + &meshtastic_Waypoint_msg, &wp)) { + // Valid waypoint + if (wp.expire > getTime()) + return devicestate.has_rx_waypoint = true; + + // Expired, or deleted + else + return devicestate.has_rx_waypoint = false; + } + + // If decoding failed + LOG_ERROR("Failed to decode waypoint"); + devicestate.has_rx_waypoint = false; + return false; +#else + return false; +#endif +} + +/// Draw the last waypoint we received +void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // Prepare to draw + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // Handle inverted display + // Unsure of expected behavior: for now, copy drawNodeInfo + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) + display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); + + // Decode the waypoint + const meshtastic_MeshPacket &mp = devicestate.rx_waypoint; + meshtastic_Waypoint wp; + memset(&wp, 0, sizeof(wp)); + if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Waypoint_msg, &wp)) { + // This *should* be caught by shouldDrawWaypoint, but we'll short-circuit here just in case + display->drawStringMaxWidth(0 + x, 0 + y, x + display->getWidth(), "Couldn't decode waypoint"); + devicestate.has_rx_waypoint = false; + return; + } + + // Get timestamp info. Will pass as a field to drawColumns + static char lastStr[20]; + screen->getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr)); + + // Will contain distance information, passed as a field to drawColumns + static char distStr[20]; + + // Get our node, to use our own position + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + + // Text fields to draw (left of compass) + // Last element must be NULL. This signals the end of the char*[] to drawColumns + const char *fields[] = {"Waypoint", lastStr, wp.name, distStr, NULL}; + + // Dimensions / co-ordinates for the compass/circle + int16_t compassX = 0, compassY = 0; + uint16_t compassDiam = graphics::Screen::getCompassDiam(display->getWidth(), display->getHeight()); + + 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; + } + + // If our node has a position: + if (ourNode && (hasValidPosition(ourNode) || screen->hasHeading())) { + const meshtastic_PositionLite &op = ourNode->position; + float myHeading; + if (screen->hasHeading()) + myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians + else + myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + screen->drawCompassNorth(display, compassX, compassY, myHeading); + + // 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) { + if (d < (2 * MILES_TO_FEET)) + snprintf(distStr, sizeof(distStr), "%.0f ft", d * METERS_TO_FEET); + else + snprintf(distStr, sizeof(distStr), "%.1f mi", d * METERS_TO_FEET / MILES_TO_FEET); + } else { + if (d < 2000) + snprintf(distStr, sizeof(distStr), "%.0f m", d); + else + snprintf(distStr, sizeof(distStr), "%.1f km", d / 1000); + } + + // 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 (!config.display.compass_north_top) + bearingToOther -= myHeading; + screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); + } + + // If our node doesn't have position + else { + // ? in the compass + display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?"); + + // ? in the distance field + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) + strncpy(distStr, "? mi", sizeof(distStr)); + else + strncpy(distStr, "? km", sizeof(distStr)); + } + + // Draw compass circle + display->drawCircle(compassX, compassY, compassDiam / 2); + + // Undo color-inversion, if set prior to drawing header + // Unsure of expected behavior? For now: copy drawNodeInfo + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + display->setColor(BLACK); + } + + // Must be after distStr is populated + screen->drawColumns(display, x, y, fields); +} +#endif diff --git a/src/modules/WaypointModule.h b/src/modules/WaypointModule.h new file mode 100644 index 0000000..4c9c7b8 --- /dev/null +++ b/src/modules/WaypointModule.h @@ -0,0 +1,33 @@ +#pragma once +#include "Observer.h" +#include "SinglePortModule.h" + +/** + * 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) {} +#if HAS_SCREEN + bool shouldDraw(); +#endif + 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 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; +#endif + 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 new file mode 100644 index 0000000..ec0ddab --- /dev/null +++ b/src/modules/esp32/AudioModule.cpp @@ -0,0 +1,291 @@ +#include "configuration.h" +#if defined(ARCH_ESP32) && defined(USE_SX1280) +#include "AudioModule.h" +#include "FSCommon.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "RTC.h" +#include "Router.h" + +/* + 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 + + Codec 2 is a low-bitrate speech audio codec (speech coding) + that is patent free and open source develop by David Grant Rowe. + http://www.rowetel.com/ and https://github.com/drowe67/codec2 + + 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) + + KNOWN PROBLEMS + * Half Duplex + * Will not work on NRF and the Linux device targets (yet?). +*/ + +ButterworthFilter hp_filter(240, 8000, ButterworthFilter::ButterworthFilter::Highpass, 1); + +TaskHandle_t codec2HandlerTask; +AudioModule *audioModule; + +#ifdef ARCH_ESP32 +// ESP32 doesn't use that flag +#define YIELD_FROM_ISR(x) portYIELD_FROM_ISR() +#else +#define YIELD_FROM_ISR(x) portYIELD_FROM_ISR(x) +#endif + +#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)); + + LOG_INFO("Starting codec2 task"); + + 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]); + + 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("Sending %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); + } + } + } + } +} + +AudioModule::AudioModule() : SinglePortModule("AudioModule", meshtastic_PortNum_AUDIO_APP), concurrency::OSThread("AudioModule") +{ + // 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("Setting 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("using %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("Initializing 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("Initializing 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("Sending %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. + 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 new file mode 100644 index 0000000..b676270 --- /dev/null +++ b/src/modules/esp32/AudioModule.h @@ -0,0 +1,87 @@ +#pragma once + +#include "SinglePortModule.h" +#include "concurrency/NotifiedWorkerThread.h" +#include "configuration.h" +#if defined(ARCH_ESP32) && defined(USE_SX1280) +#include "NodeDB.h" +#include +#include +#include +#include +#include +#include +#include + +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; +}; + +#define ADC_BUFFER_SIZE_MAX 320 +#define PTT_PIN 39 + +#define I2S_PORT I2S_NUM_0 + +#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; + + struct CODEC2 *codec2 = NULL; + // int16_t sample; + + AudioModule(); + + bool shouldDraw(); + + /** + * Send our payload into the mesh + */ + void sendPayload(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); + + protected: + int encode_frame_num = 0; + bool firstTime = true; + + virtual int32_t runOnce() override; + + virtual meshtastic_MeshPacket *allocReply() override; + + 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); +#else + 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; +}; + +extern AudioModule *audioModule; + +#endif \ No newline at end of file diff --git a/src/modules/esp32/PaxcounterModule.cpp b/src/modules/esp32/PaxcounterModule.cpp new file mode 100644 index 0000000..3a0f14c --- /dev/null +++ b/src/modules/esp32/PaxcounterModule.cpp @@ -0,0 +1,130 @@ +#include "configuration.h" +#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_PAXCOUNTER +#include "Default.h" +#include "MeshService.h" +#include "PaxcounterModule.h" +#include + +PaxcounterModule *paxcounterModule; + +/** + * Callback function for libpax. + * 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); +} + +PaxcounterModule::PaxcounterModule() + : concurrency::OSThread("PaxcounterModule"), + ProtobufModule("paxcounter", meshtastic_PortNum_PAXCOUNTER_APP, &meshtastic_Paxcount_msg) +{ +} + +/** + * Send the Pax information to the mesh if we got new data from libpax. + * This is called periodically from our runOnce() method and will actually send the data to the mesh + * if libpax updated it since the last transmission through the callback. + * @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; + + LOG_INFO("PaxcounterModule: sending 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_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); + + paxcounterModule->reportedDataSent = 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. +} + +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); + + 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, + moduleConfig.paxcounter.paxcounter_update_interval, 0); + libpax_counter_start(); + } else { + sendInfo(NODENUM_BROADCAST); + } + return Default::getConfiguredOrDefaultMsScaled(moduleConfig.paxcounter.paxcounter_update_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes); + } else { + return disable(); + } +} + +#if HAS_SCREEN + +#include "graphics/ScreenFonts.h" + +void PaxcounterModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + char buffer[50]; + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawString(x + 0, y + 0, "PAX"); + + libpax_counter_count(&count_from_libpax); + + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_SMALL); + display->drawStringf(display->getWidth() / 2 + x, 0 + y + 12, buffer, "WiFi: %d\nBLE: %d\nuptime: %ds", + count_from_libpax.wifi_count, count_from_libpax.ble_count, millis() / 1000); +} +#endif // HAS_SCREEN + +#endif \ No newline at end of file diff --git a/src/modules/esp32/PaxcounterModule.h b/src/modules/esp32/PaxcounterModule.h new file mode 100644 index 0000000..ebd6e71 --- /dev/null +++ b/src/modules/esp32/PaxcounterModule.h @@ -0,0 +1,38 @@ +#pragma once + +#include "ProtobufModule.h" +#include "configuration.h" +#if defined(ARCH_ESP32) +#include "../mesh/generated/meshtastic/paxcount.pb.h" +#include "NodeDB.h" +#include + +/** + * 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; + + static void handlePaxCounterReportRequest(); + + 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; } +#if HAS_SCREEN + virtual bool wantUIFrame() override { return isActive(); } + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; +#endif +}; + +extern PaxcounterModule *paxcounterModule; +#endif \ No newline at end of file diff --git a/src/motion/AccelerometerThread.h b/src/motion/AccelerometerThread.h new file mode 100644 index 0000000..e548b20 --- /dev/null +++ b/src/motion/AccelerometerThread.h @@ -0,0 +1,157 @@ +#pragma once +#ifndef _ACCELEROMETER_H_ +#define _ACCELEROMETER_H_ + +#include "configuration.h" + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C + +#include "../concurrency/OSThread.h" +#include "BMA423Sensor.h" +#include "BMX160Sensor.h" +#include "ICM20948Sensor.h" +#include "LIS3DHSensor.h" +#include "LSM6DS3Sensor.h" +#include "MPU6050Sensor.h" +#include "MotionSensor.h" +#ifdef HAS_QMA6100P +#include "QMA6100PSensor.h" +#endif +#include "STK8XXXSensor.h" + +extern ScanI2C::DeviceAddress accelerometer_found; + +class AccelerometerThread : public concurrency::OSThread +{ + private: + MotionSensor *sensor = nullptr; + bool isInitialised = false; + + public: + explicit AccelerometerThread(ScanI2C::FoundDevice foundDevice) : OSThread("AccelerometerThread") + { + device = foundDevice; + init(); + } + + explicit AccelerometerThread(ScanI2C::DeviceType type) : AccelerometerThread(ScanI2C::FoundDevice{type, accelerometer_found}) + { + } + + void start() + { + init(); + setIntervalFromNow(0); + }; + + 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 disabling due to no sensors found"); + disable(); + return; + } + +#ifndef RAK_4631 + if (!config.display.wake_on_tap_or_motion && !config.device.double_tap_as_button_press) { + LOG_DEBUG("AccelerometerThread disabling due to no interested configurations"); + disable(); + return; + } +#endif + + switch (device.type) { + case ScanI2C::DeviceType::BMA423: + sensor = new BMA423Sensor(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; + case ScanI2C::DeviceType::STK8BAXX: + sensor = new STK8XXXSensor(device); + break; + case ScanI2C::DeviceType::ICM20948: + sensor = new ICM20948Sensor(device); + break; +#ifdef HAS_QMA6100P + 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"); + } + + // Copy constructor (not implemented / included to avoid cppcheck warnings) + AccelerometerThread(const AccelerometerThread &other) : OSThread::OSThread("AccelerometerThread") { 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)); + } + } + + // Cleanup resources + void clean() + { + isInitialised = false; + if (sensor != nullptr) { + delete sensor; + sensor = nullptr; + } + } +}; + +#endif + +#endif \ No newline at end of file diff --git a/src/motion/BMA423Sensor.cpp b/src/motion/BMA423Sensor.cpp new file mode 100644 index 0000000..ec88a94 --- /dev/null +++ b/src/motion/BMA423Sensor.cpp @@ -0,0 +1,62 @@ +#include "BMA423Sensor.h" + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C + +using namespace MotionSensorI2C; + +BMA423Sensor::BMA423Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} + +bool BMA423Sensor::init() +{ + if (sensor.begin(deviceAddress(), &MotionSensorI2C::readRegister, &MotionSensorI2C::writeRegister)) { + sensor.configAccelerometer(sensor.RANGE_2G, sensor.ODR_100HZ, sensor.BW_NORMAL_AVG4, sensor.PERF_CONTINUOUS_MODE); + sensor.enableAccelerometer(); + sensor.configInterrupt(BMA4_LEVEL_TRIGGER, BMA4_ACTIVE_HIGH, BMA4_PUSH_PULL, BMA4_OUTPUT_ENABLE, BMA4_INPUT_DISABLE); + +#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 +#endif + +#ifdef T_WATCH_S3 + // Need to raise the wrist function, need to set the correct axis + sensor.setReampAxes(sensor.REMAP_TOP_LAYER_RIGHT_CORNER); +#else + sensor.setReampAxes(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(); + + // Turn on feature interrupt + sensor.enablePedometerIRQ(); + sensor.enableTiltIRQ(); + + // It corresponds to isDoubleClick interrupt + sensor.enableWakeupIRQ(); + LOG_DEBUG("BMA423Sensor::init ok"); + return true; + } + LOG_DEBUG("BMA423Sensor::init failed"); + return false; +} + +int32_t BMA423Sensor::runOnce() +{ + if (sensor.readIrqStatus() != DEV_WIRE_NONE) { + if (sensor.isTilt() || sensor.isDoubleTap()) { + wakeScreen(); + return 500; + } + } + 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 new file mode 100644 index 0000000..0bc0ddf --- /dev/null +++ b/src/motion/BMA423Sensor.h @@ -0,0 +1,26 @@ +#pragma once +#ifndef _BMA423_SENSOR_H_ +#define _BMA423_SENSOR_H_ + +#include "MotionSensor.h" + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C + +#include +#include + +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; +}; + +#endif + +#endif \ No newline at end of file diff --git a/src/motion/BMX160Sensor.cpp b/src/motion/BMX160Sensor.cpp new file mode 100644 index 0000000..f9e82e8 --- /dev/null +++ b/src/motion/BMX160Sensor.cpp @@ -0,0 +1,104 @@ +#include "BMX160Sensor.h" + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C + +BMX160Sensor::BMX160Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} + +#ifdef RAK_4631 +#if !defined(MESHTASTIC_EXCLUDE_SCREEN) + +// screen is defined in main.cpp +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("BMX160Sensor::init ok"); + return true; + } + LOG_DEBUG("BMX160Sensor::init failed"); + return false; +} + +int32_t BMX160Sensor::runOnce() +{ + sBmx160SensorData_t magAccel; + sBmx160SensorData_t gAccel; + + /* Get a new sensor event */ + sensor.getAllData(&magAccel, NULL, &gAccel); + +#if !defined(MESHTASTIC_EXCLUDE_SCREEN) + // experimental calibrate routine. Limited to between 10 and 30 seconds after boot + if (millis() > 12 * 1000 && millis() < 30 * 1000) { + if (!showingScreen) { + showingScreen = true; + 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; + } else if (showingScreen && millis() >= 30 * 1000) { + showingScreen = false; + screen->endAlert(); + } + + 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; + } + screen->setHeading(heading); +#endif + + return MOTION_SENSOR_CHECK_INTERVAL_MS; +} + +#endif + +#endif \ No newline at end of file diff --git a/src/motion/BMX160Sensor.h b/src/motion/BMX160Sensor.h new file mode 100644 index 0000000..26f4772 --- /dev/null +++ b/src/motion/BMX160Sensor.h @@ -0,0 +1,41 @@ +#pragma once + +#ifndef _BMX160_SENSOR_H_ +#define _BMX160_SENSOR_H_ + +#include "MotionSensor.h" + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C + +#ifdef RAK_4631 + +#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; + + public: + explicit BMX160Sensor(ScanI2C::FoundDevice foundDevice); + virtual bool init() override; + virtual int32_t runOnce() override; +}; + +#else + +// Stub +class BMX160Sensor : public MotionSensor +{ + public: + explicit BMX160Sensor(ScanI2C::FoundDevice foundDevice); +}; + +#endif + +#endif + +#endif \ No newline at end of file diff --git a/src/motion/ICM20948Sensor.cpp b/src/motion/ICM20948Sensor.cpp new file mode 100644 index 0000000..d1272e8 --- /dev/null +++ b/src/motion/ICM20948Sensor.cpp @@ -0,0 +1,186 @@ +#include "ICM20948Sensor.h" + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C + +// Flag when an interrupt has been detected +volatile static bool ICM20948_IRQ = false; + +// Interrupt service routine +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; + + // 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; +} + +#else + +int32_t ICM20948Sensor::runOnce() +{ + // 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("ICM20948Sensor::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("ICM20948Sensor::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 + +// ---------------------------------------------------------------------- +// ICM20948Singleton +// ---------------------------------------------------------------------- + +// Get a singleton wrapper for an Sparkfun ICM_20948_I2C +ICM20948Singleton *ICM20948Singleton::GetInstance() +{ + if (pinstance == nullptr) { + pinstance = new ICM20948Singleton(); + } + return pinstance; +} + +ICM20948Singleton::ICM20948Singleton() {} + +ICM20948Singleton::~ICM20948Singleton() {} + +ICM20948Singleton *ICM20948Singleton::pinstance{nullptr}; + +// Initialise the ICM20948 Sensor +bool ICM20948Singleton::init(ScanI2C::FoundDevice device) +{ +#ifdef ICM_20948_DEBUG + // Set ICM_20948_DEBUG to enable helpful debug messages on Serial + enableDebugging(); +#endif + +// startup +#ifdef Wire1 + ICM_20948_Status_e status = + begin(device.address.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire, device.address.address == ICM20948_ADDR ? 1 : 0); +#else + ICM_20948_Status_e status = begin(Wire, device.address.address == ICM20948_ADDR ? 1 : 0); +#endif + if (status != ICM_20948_Stat_Ok) { + LOG_DEBUG("ICM20948Sensor::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("ICM20948Sensor::init reset - %s", statusString()); + return false; + } + delay(200); + + // Now wake the sensor up + if (sleep(false) != ICM_20948_Stat_Ok) { + LOG_DEBUG("ICM20948Sensor::init wake - %s", statusString()); + return false; + } + + if (lowPower(false) != ICM_20948_Stat_Ok) { + LOG_DEBUG("ICM20948Sensor::init high power - %s", statusString()); + return false; + } + +#ifdef ICM_20948_INT_PIN + + // Active low + cfgIntActiveLow(true); + LOG_DEBUG("ICM20948Sensor::init set cfgIntActiveLow - %s", statusString()); + + // Push-pull + cfgIntOpenDrain(false); + LOG_DEBUG("ICM20948Sensor::init set cfgIntOpenDrain - %s", statusString()); + + // If enabled, *ANY* read will clear the INT_STATUS register. + cfgIntAnyReadToClear(true); + LOG_DEBUG("ICM20948Sensor::init set cfgIntAnyReadToClear - %s", statusString()); + + // Latch the interrupt until cleared + cfgIntLatch(true); + LOG_DEBUG("ICM20948Sensor::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 service routine + attachInterrupt(ICM_20948_INT_PIN, ICM20948SetInterrupt, FALLING); + +#endif + 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) + return false; + + // Enable WoM Logic mode 1 = Compare the current sample with the previous sample + status = WOMLogic(true, 1); + LOG_DEBUG("ICM20948Sensor::init set WOMLogic - %s", statusString()); + if (status != ICM_20948_Stat_Ok) + return false; + + // Enable interrupts on WakeOnMotion + status = intEnableWOM(true); + LOG_DEBUG("ICM20948Sensor::init set intEnableWOM - %s", statusString()); + return status == ICM_20948_Stat_Ok; + + // Clear any current interrupts + ICM20948_IRQ = false; + clearInterrupts(); + return true; +} + +#endif \ No newline at end of file diff --git a/src/motion/ICM20948Sensor.h b/src/motion/ICM20948Sensor.h new file mode 100644 index 0000000..d5e246c --- /dev/null +++ b/src/motion/ICM20948Sensor.h @@ -0,0 +1,96 @@ +#pragma once +#ifndef _ICM_20948_SENSOR_H_ +#define _ICM_20948_SENSOR_H_ + +#include "MotionSensor.h" + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C + +#include + +// Set the default gyro scale - dps250, dps500, dps1000, dps2000 +#ifndef ICM_20948_MPU_GYRO_SCALE +#define ICM_20948_MPU_GYRO_SCALE dps250 +#endif + +// Set the default accelerometer scale - gpm2, gpm4, gpm8, gpm16 +#ifndef ICM_20948_MPU_ACCEL_SCALE +#define ICM_20948_MPU_ACCEL_SCALE gpm2 +#endif + +// Define a threshold for Wake on Motion Sensing (0mg to 1020mg) +#ifndef ICM_20948_WOM_THRESHOLD +#define ICM_20948_WOM_THRESHOLD 16U +#endif + +// Define a pin in variant.h to use interrupts to read the ICM-20948 +#ifndef ICM_20948_WOM_THRESHOLD +#define ICM_20948_INT_PIN 255 +#endif + +// Uncomment this line to enable helpful debug messages on Serial +// #define ICM_20948_DEBUG 1 + +// Uncomment this line to enable the onboard digital motion processor (to be added in a future PR) +// #define ICM_20948_DMP_IS_ENABLED 1 + +// Check for a mandatory compiler flag to use the DMP (to be added in a future PR) +#ifdef ICM_20948_DMP_IS_ENABLED +#ifndef ICM_20948_USE_DMP +#error To use the digital motion processor, please either set the compiler flag ICM_20948_USE_DMP or uncomment line 29 (#define ICM_20948_USE_DMP) in ICM_20948_C.h +#endif +#endif + +// The I2C address of the Accelerometer (if found) from main.cpp +extern ScanI2C::DeviceAddress accelerometer_found; + +// Singleton wrapper for the Sparkfun ICM_20948_I2C class +class ICM20948Singleton : public ICM_20948_I2C +{ + private: + static ICM20948Singleton *pinstance; + + protected: + ICM20948Singleton(); + ~ICM20948Singleton(); + + public: + // Create a singleton instance (not thread safe) + static ICM20948Singleton *GetInstance(); + + // Singletons should not be cloneable. + ICM20948Singleton(ICM20948Singleton &other) = delete; + + // Singletons should not be assignable. + void operator=(const ICM20948Singleton &) = delete; + + // 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(); + +#ifdef ICM_20948_DMP_IS_ENABLED + // Initialise the motion sensor singleton for digital motion processing + bool initDMP(); +#endif +}; + +class ICM20948Sensor : public MotionSensor +{ + private: + ICM20948Singleton *sensor = nullptr; + + public: + explicit ICM20948Sensor(ScanI2C::FoundDevice foundDevice); + + // Initialise the motion sensor + virtual bool init() override; + + // Called each time our sensor gets a chance to run + virtual int32_t runOnce() override; +}; + +#endif + +#endif \ No newline at end of file diff --git a/src/motion/LIS3DHSensor.cpp b/src/motion/LIS3DHSensor.cpp new file mode 100644 index 0000000..d06b46b --- /dev/null +++ b/src/motion/LIS3DHSensor.cpp @@ -0,0 +1,37 @@ +#include "LIS3DHSensor.h" +#include "NodeDB.h" + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C + +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("LIS3DHSensor::init ok"); + return true; + } + LOG_DEBUG("LIS3DHSensor::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) { + wakeScreen(); + } + + if (config.device.double_tap_as_button_press && (click & 0x20)) { + buttonPress(); + return 500; + } + } + return MOTION_SENSOR_CHECK_INTERVAL_MS; +} + +#endif \ No newline at end of file diff --git a/src/motion/LIS3DHSensor.h b/src/motion/LIS3DHSensor.h new file mode 100644 index 0000000..603d195 --- /dev/null +++ b/src/motion/LIS3DHSensor.h @@ -0,0 +1,24 @@ +#pragma once +#ifndef _LIS3DH_SENSOR_H_ +#define _LIS3DH_SENSOR_H_ + +#include "MotionSensor.h" + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C + +#include + +class LIS3DHSensor : public MotionSensor +{ + private: + Adafruit_LIS3DH sensor; + + public: + explicit LIS3DHSensor(ScanI2C::FoundDevice foundDevice); + virtual bool init() override; + virtual int32_t runOnce() override; +}; + +#endif + +#endif \ No newline at end of file diff --git a/src/motion/LSM6DS3Sensor.cpp b/src/motion/LSM6DS3Sensor.cpp new file mode 100644 index 0000000..3b25c38 --- /dev/null +++ b/src/motion/LSM6DS3Sensor.cpp @@ -0,0 +1,34 @@ +#include "LSM6DS3Sensor.h" +#include "NodeDB.h" + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C + +LSM6DS3Sensor::LSM6DS3Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} + +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); + + // Duration is number of occurances needed to trigger, higher threshold is less sensitive + sensor.enableWakeup(config.display.wake_on_tap_or_motion, 1, LSM6DS3_WAKE_THRESH); + + LOG_DEBUG("LSM6DS3Sensor::init ok"); + return true; + } + LOG_DEBUG("LSM6DS3Sensor::init failed"); + return false; +} + +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 new file mode 100644 index 0000000..77069ef --- /dev/null +++ b/src/motion/LSM6DS3Sensor.h @@ -0,0 +1,28 @@ +#pragma once +#ifndef _LSM6DS3_SENSOR_H_ +#define _LSM6DS3_SENSOR_H_ + +#include "MotionSensor.h" + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C + +#ifndef LSM6DS3_WAKE_THRESH +#define LSM6DS3_WAKE_THRESH 20 +#endif + +#include + +class LSM6DS3Sensor : public MotionSensor +{ + private: + Adafruit_LSM6DS3TRC sensor; + + public: + explicit LSM6DS3Sensor(ScanI2C::FoundDevice foundDevice); + virtual bool init() override; + virtual int32_t runOnce() override; +}; + +#endif + +#endif \ No newline at end of file diff --git a/src/motion/MPU6050Sensor.cpp b/src/motion/MPU6050Sensor.cpp new file mode 100644 index 0000000..b504809 --- /dev/null +++ b/src/motion/MPU6050Sensor.cpp @@ -0,0 +1,31 @@ +#include "MPU6050Sensor.h" + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C + +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("MPU6050Sensor::init ok"); + return true; + } + LOG_DEBUG("MPU6050Sensor::init failed"); + return false; +} + +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 new file mode 100644 index 0000000..2e6eafe --- /dev/null +++ b/src/motion/MPU6050Sensor.h @@ -0,0 +1,24 @@ +#pragma once +#ifndef _MPU6050_SENSOR_H_ +#define _MPU6050_SENSOR_H_ + +#include "MotionSensor.h" + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C + +#include + +class MPU6050Sensor : public MotionSensor +{ + private: + Adafruit_MPU6050 sensor; + + public: + explicit MPU6050Sensor(ScanI2C::FoundDevice foundDevice); + virtual bool init() override; + virtual int32_t runOnce() override; +}; + +#endif + +#endif \ No newline at end of file diff --git a/src/motion/MotionSensor.cpp b/src/motion/MotionSensor.cpp new file mode 100644 index 0000000..95bf646 --- /dev/null +++ b/src/motion/MotionSensor.cpp @@ -0,0 +1,79 @@ +#include "MotionSensor.h" + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C + +// 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("MotionSensor::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; +} + +uint8_t MotionSensor::deviceAddress() +{ + return device.address.address; +} + +ScanI2C::I2CPort MotionSensor::devicePort() +{ + return device.address.port; +} + +#if defined(RAK_4631) & !MESHTASTIC_EXCLUDE_SCREEN +void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // 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"); + int16_t compassX = 0, compassY = 0; + uint16_t compassDiam = graphics::Screen::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); + screen->drawCompassNorth(display, compassX, compassY, screen->getHeading() * PI / 180); +} +#endif + +#if !MESHTASTIC_EXCLUDE_POWER_FSM +void MotionSensor::wakeScreen() +{ + if (powerFSM.getState() == &stateDARK) { + LOG_DEBUG("MotionSensor::wakeScreen detected"); + powerFSM.trigger(EVENT_INPUT); + } +} + +void MotionSensor::buttonPress() +{ + LOG_DEBUG("MotionSensor::buttonPress detected"); + powerFSM.trigger(EVENT_PRESS); +} + +#else + +void MotionSensor::wakeScreen() {} + +void MotionSensor::buttonPress() {} + +#endif + +#endif \ No newline at end of file diff --git a/src/motion/MotionSensor.h b/src/motion/MotionSensor.h new file mode 100644 index 0000000..78eec54 --- /dev/null +++ b/src/motion/MotionSensor.h @@ -0,0 +1,86 @@ +#pragma once +#ifndef _MOTION_SENSOR_H_ +#define _MOTION_SENSOR_H_ + +#define MOTION_SENSOR_CHECK_INTERVAL_MS 100 +#define MOTION_SENSOR_CLICK_THRESHOLD 40 + +#include "../configuration.h" + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C + +#include "../PowerFSM.h" +#include "../detect/ScanI2C.h" +#include "../graphics/Screen.h" +#include "../graphics/ScreenFonts.h" +#include "../power.h" +#include "Wire.h" + +// Base class for motion processing +class MotionSensor +{ + public: + explicit MotionSensor(ScanI2C::FoundDevice foundDevice); + virtual ~MotionSensor(){}; + + // Get the device type + ScanI2C::DeviceType deviceType(); + + // Get the device address + uint8_t deviceAddress(); + + // Get the device port + ScanI2C::I2CPort devicePort(); + + // 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; }; + + 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(); + +#if defined(RAK_4631) & !MESHTASTIC_EXCLUDE_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); +#endif + + ScanI2C::FoundDevice device; +}; + +namespace MotionSensorI2C +{ + +static inline int readRegister(uint8_t address, uint8_t reg, uint8_t *data, uint8_t len) +{ + Wire.beginTransmission(address); + Wire.write(reg); + Wire.endTransmission(); + Wire.requestFrom((uint8_t)address, (uint8_t)len); + uint8_t i = 0; + while (Wire.available()) { + data[i++] = Wire.read(); + } + return 0; // Pass +} + +static inline int writeRegister(uint8_t address, uint8_t reg, uint8_t *data, uint8_t len) +{ + Wire.beginTransmission(address); + Wire.write(reg); + Wire.write(data, len); + return (0 != Wire.endTransmission()); +} + +} // namespace MotionSensorI2C + +#endif + +#endif \ No newline at end of file diff --git a/src/motion/QMA6100PSensor.cpp b/src/motion/QMA6100PSensor.cpp new file mode 100644 index 0000000..989188f --- /dev/null +++ b/src/motion/QMA6100PSensor.cpp @@ -0,0 +1,183 @@ +#include "QMA6100PSensor.h" + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && defined(HAS_QMA6100P) + +// Flag when an interrupt has been detected +volatile static bool QMA6100P_IRQ = false; + +// Interrupt service routine +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; + + // 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; +} + +#else + +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("QMA6100PSensor::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 + +// ---------------------------------------------------------------------- +// QMA6100PSingleton +// ---------------------------------------------------------------------- + +// Get a singleton wrapper for an Sparkfun QMA_6100P_I2C +QMA6100PSingleton *QMA6100PSingleton::GetInstance() +{ + if (pinstance == nullptr) { + pinstance = new QMA6100PSingleton(); + } + return pinstance; +} + +QMA6100PSingleton::QMA6100PSingleton() {} + +QMA6100PSingleton::~QMA6100PSingleton() {} + +QMA6100PSingleton *QMA6100PSingleton::pinstance{nullptr}; + +// Initialise the QMA6100P Sensor +bool QMA6100PSingleton::init(ScanI2C::FoundDevice device) +{ + +// startup +#ifdef Wire1 + 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); +#endif + if (status != true) { + LOG_WARN("QMA6100PSensor::init begin failed\n"); + return false; + } + delay(20); + // SW reset to make sure the device starts in a known state + if (softwareReset() != true) { + LOG_WARN("QMA6100PSensor::init reset failed\n"); + return false; + } + delay(20); + // Set range + if (!setRange(QMA_6100P_MPU_ACCEL_SCALE)) { + LOG_WARN("QMA6100PSensor::init range failed"); + return false; + } + // set active mode + if (!enableAccel()) { + LOG_WARN("ERROR :QMA6100PSensor::active mode set failed"); + } + // set calibrateoffsets + if (!calibrateOffsets()) { + LOG_WARN("ERROR :QMA6100PSensor:: calibration failed"); + } +#ifdef QMA_6100P_INT_PIN + + // Active low & Open Drain + uint8_t tempVal; + if (!readRegisterRegion(SFE_QMA6100P_INTPINT_CONF, &tempVal, 1)) { + LOG_WARN("QMA6100PSensor::init failed to read interrupt pin config"); + return false; + } + + tempVal |= 0b00000010; // Active low & Open Drain + + if (!writeRegisterByte(SFE_QMA6100P_INTPINT_CONF, tempVal)) { + LOG_WARN("QMA6100PSensor::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("QMA6100PSensor::init failed to read interrupt config"); + return false; + } + + tempVal |= 0b10000001; // Latch until cleared, INT_RD_CLR1 + + if (!writeRegisterByte(SFE_QMA6100P_INT_CFG, tempVal)) { + LOG_WARN("QMA6100PSensor::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); + +#endif + return true; +} + +bool QMA6100PSingleton::setWakeOnMotion() +{ + // Enable 'Any Motion' interrupt + if (!writeRegisterByte(SFE_QMA6100P_INT_EN2, 0b00000111)) { + LOG_WARN("QMA6100PSingleton::setWakeOnMotion failed to write interrupt enable"); + return false; + } + + // Set 'Significant Motion' interrupt map to INT1 + uint8_t tempVal; + + if (!readRegisterRegion(SFE_QMA6100P_INT_MAP1, &tempVal, 1)) { + LOG_WARN("QMA6100PSingleton::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; + + if (!writeRegisterByte(SFE_QMA6100P_INT_MAP1, tempVal)) { + LOG_WARN("QMA6100PSingleton::setWakeOnMotion failed to write interrupt map"); + return false; + } + + // Clear any current interrupts + QMA6100P_IRQ = false; + return true; +} + +#endif \ No newline at end of file diff --git a/src/motion/QMA6100PSensor.h b/src/motion/QMA6100PSensor.h new file mode 100644 index 0000000..7ba0014 --- /dev/null +++ b/src/motion/QMA6100PSensor.h @@ -0,0 +1,63 @@ +#pragma once +#ifndef _QMA_6100P_SENSOR_H_ +#define _QMA_6100P_SENSOR_H_ + +#include "MotionSensor.h" + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && defined(HAS_QMA6100P) + +#include + +// Set the default accelerometer scale - gpm2, gpm4, gpm8, gpm16 +#ifndef QMA_6100P_MPU_ACCEL_SCALE +#define QMA_6100P_MPU_ACCEL_SCALE SFE_QMA6100P_RANGE32G +#endif + +// The I2C address of the Accelerometer (if found) from main.cpp +extern ScanI2C::DeviceAddress accelerometer_found; + +// Singleton wrapper for the Sparkfun QMA_6100P_I2C class +class QMA6100PSingleton : public QMA6100P +{ + private: + static QMA6100PSingleton *pinstance; + + protected: + QMA6100PSingleton(); + ~QMA6100PSingleton(); + + public: + // Create a singleton instance (not thread safe) + static QMA6100PSingleton *GetInstance(); + + // Singletons should not be cloneable. + QMA6100PSingleton(QMA6100PSingleton &other) = delete; + + // Singletons should not be assignable. + void operator=(const QMA6100PSingleton &) = delete; + + // 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(); +}; + +class QMA6100PSensor : public MotionSensor +{ + private: + QMA6100PSingleton *sensor = nullptr; + + public: + explicit QMA6100PSensor(ScanI2C::FoundDevice foundDevice); + + // Initialise the motion sensor + virtual bool init() override; + + // Called each time our sensor gets a chance to run + virtual int32_t runOnce() override; +}; + +#endif + +#endif \ No newline at end of file diff --git a/src/motion/STK8XXXSensor.cpp b/src/motion/STK8XXXSensor.cpp new file mode 100644 index 0000000..72b4bc3 --- /dev/null +++ b/src/motion/STK8XXXSensor.cpp @@ -0,0 +1,40 @@ +#include "STK8XXXSensor.h" + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C + +STK8XXXSensor::STK8XXXSensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} + +#ifdef STK8XXX_INT + +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); + + LOG_DEBUG("STK8XXXSensor::init ok"); + return true; + } + LOG_DEBUG("STK8XXXSensor::init failed"); + return false; +} + +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; +} + +#endif + +#endif \ No newline at end of file diff --git a/src/motion/STK8XXXSensor.h b/src/motion/STK8XXXSensor.h new file mode 100644 index 0000000..190b916 --- /dev/null +++ b/src/motion/STK8XXXSensor.h @@ -0,0 +1,37 @@ +#pragma once +#ifndef _STK8XXX_SENSOR_H_ +#define _STK8XXX_SENSOR_H_ + +#include "MotionSensor.h" + +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C + +#ifdef STK8XXX_INT + +#include + +class STK8XXXSensor : public MotionSensor +{ + private: + STK8xxx sensor; + + 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); +}; + +#endif + +#endif + +#endif \ No newline at end of file diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp new file mode 100644 index 0000000..39d5541 --- /dev/null +++ b/src/mqtt/MQTT.cpp @@ -0,0 +1,771 @@ +#include "MQTT.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "PowerFSM.h" +#include "configuration.h" +#include "main.h" +#include "mesh/Channels.h" +#include "mesh/Router.h" +#include "mesh/generated/meshtastic/mqtt.pb.h" +#include "mesh/generated/meshtastic/telemetry.pb.h" +#include "modules/RoutingModule.h" +#if defined(ARCH_ESP32) +#include "../mesh/generated/meshtastic/paxcount.pb.h" +#endif +#include "mesh/generated/meshtastic/remote_hardware.pb.h" +#include "sleep.h" +#if HAS_WIFI +#include "mesh/wifi/WiFiAPClient.h" +#include +#endif +#include "Default.h" +#include "serialization/JSON.h" +#include "serialization/MeshPacketSerializer.h" +#include +#include + +const int reconnectMax = 5; + +MQTT *mqtt; + +static MemoryDynamic staticMqttPool; + +Allocator &mqttPool = staticMqttPool; + +// FIXME - this size calculation is super sloppy, but it will go away once we dynamically alloc meshpackets +static uint8_t bytes[meshtastic_MqttClientProxyMessage_size + 30]; // 12 for channel name and 16 for nodeid + +static bool isMqttServerAddressPrivate = false; + +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::onReceive(char *topic, byte *payload, size_t length) +{ + meshtastic_ServiceEnvelope e = meshtastic_ServiceEnvelope_init_default; + + 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 + char payloadStr[length + 1]; + memcpy(payloadStr, payload, length); + payloadStr[length] = 0; // null terminated string + JSONValue *json_value = JSON::Parse(payloadStr); + if (json_value != NULL) { + // check if it is a valid envelope + JSONObject json; + json = json_value->AsObject(); + + // parse the channel name from the topic string + // the topic has been checked above for having jsonTopic prefix, so just move past it + char *ptr = topic + jsonTopic.length(); + ptr = strtok(ptr, "/") ? strtok(ptr, "/") : ptr; // if another "/" was added, parse string up to that character + meshtastic_Channel sendChannel = channels.getByName(ptr); + // We allow downlink JSON packets only on a channel named "mqtt" + if (strncasecmp(channels.getGlobalId(sendChannel.index), Channels::mqttChannel, strlen(Channels::mqttChannel)) == 0 && + sendChannel.settings.downlink_enabled) { + if (isValidJsonEnvelope(json)) { + // 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, dropping"); + } + } 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 Ignoring downlink message with unsupported type."); + } + } else { + LOG_ERROR("JSON Received payload on MQTT but not a valid envelope."); + } + } else { + LOG_WARN("JSON downlink received on channel not called 'mqtt' or without downlink enabled."); + } + } else { + // no json, this is an invalid payload + LOG_ERROR("JSON Received payload on MQTT but not a valid JSON"); + } + delete json_value; + } else { + if (length == 0) { + LOG_WARN("Empty MQTT payload received, topic %s!", topic); + return; + } else if (!pb_decode_from_bytes(payload, length, &meshtastic_ServiceEnvelope_msg, &e)) { + LOG_ERROR("Invalid MQTT service envelope, topic %s, len %u!", topic, length); + return; + } else { + if (e.channel_id == NULL || e.gateway_id == NULL) { + LOG_ERROR("Invalid MQTT service envelope, topic %s, len %u!", topic, length); + return; + } + meshtastic_Channel ch = channels.getByName(e.channel_id); + if (strcmp(e.gateway_id, owner.id) == 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 (e.packet && isFromUs(e.packet)) + routingModule->sendAckNak(meshtastic_Routing_Error_NONE, getFrom(e.packet), e.packet->id, ch.index); + else + LOG_INFO("Ignoring downlink message we originally sent."); + } else { + // Find channel by channel_id and check downlink_enabled + if ((strcmp(e.channel_id, "PKI") == 0 && e.packet) || + (strcmp(e.channel_id, channels.getGlobalId(ch.index)) == 0 && e.packet && ch.settings.downlink_enabled)) { + LOG_INFO("Received MQTT topic %s, len=%u", topic, length); + meshtastic_MeshPacket *p = packetPool.allocCopy(*e.packet); + p->via_mqtt = true; // Mark that the packet was received via MQTT + + if (isFromUs(p)) { + LOG_INFO("Ignoring downlink message we originally sent."); + packetPool.release(p); + return; + } + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + if (moduleConfig.mqtt.encryption_enabled) { + LOG_INFO("Ignoring decoded message on MQTT, encryption is enabled."); + packetPool.release(p); + return; + } + if (p->decoded.portnum == meshtastic_PortNum_ADMIN_APP) { + LOG_INFO("Ignoring decoded admin packet."); + packetPool.release(p); + 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)); + 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) || (tx && tx->has_user && rx && rx->has_user)) + router->enqueueReceivedMessage(p); + } else if (router && perhapsDecode(p)) // ignore messages if we don't have the channel key + router->enqueueReceivedMessage(p); + else + packetPool.release(p); + } + } + } + // make sure to free both strings and the MeshPacket (passing in NULL is acceptable) + free(e.channel_id); + free(e.gateway_id); + free(e.packet); + } +} + +void mqttInit() +{ + new MQTT(); +} + +#if HAS_NETWORKING +MQTT::MQTT() : concurrency::OSThread("mqtt"), pubSub(mqttClient), mqttQueue(MAX_MQTT_QUEUE) +#else +MQTT::MQTT() : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE) +#endif +{ + if (moduleConfig.mqtt.enabled) { + LOG_DEBUG("Initializing MQTT"); + + assert(!mqtt); + mqtt = this; + + if (*moduleConfig.mqtt.root) { + cryptTopic = moduleConfig.mqtt.root + cryptTopic; + jsonTopic = moduleConfig.mqtt.root + jsonTopic; + mapTopic = moduleConfig.mqtt.root + mapTopic; + } else { + cryptTopic = "msh" + cryptTopic; + jsonTopic = "msh" + jsonTopic; + mapTopic = "msh" + mapTopic; + } + + 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); + } + + isMqttServerAddressPrivate = isPrivateIpAddress(moduleConfig.mqtt.address); + if (isMqttServerAddressPrivate) { + LOG_INFO("MQTT server is a private IP address."); + } + +#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() +{ +#if HAS_NETWORKING + return pubSub.connected(); +#else + 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; + strcpy(msg->topic, topic); + 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() +{ + if (wantsLink()) { + if (moduleConfig.mqtt.proxy_to_client_enabled) { + LOG_INFO("MQTT connecting via client proxy instead..."); + enabled = true; + runASAP = true; + reconnectCount = 0; + + publishNodeInfo(); + return; // Don't try to connect directly to the server + } +#if HAS_NETWORKING + // Defaults + int serverPort = 1883; + const char *serverAddr = default_mqtt_address; + const char *mqttUsername = default_mqtt_username; + const char *mqttPassword = default_mqtt_password; + + if (*moduleConfig.mqtt.address) { + serverAddr = moduleConfig.mqtt.address; + mqttUsername = moduleConfig.mqtt.username; + mqttPassword = moduleConfig.mqtt.password; + } +#if HAS_WIFI && !defined(ARCH_PORTDUINO) +#if !defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(RPI_PICO) + if (moduleConfig.mqtt.tls_enabled) { + // change default for encrypted to 8883 + try { + serverPort = 8883; + wifiSecureClient.setInsecure(); + + pubSub.setClient(wifiSecureClient); + LOG_INFO("Using TLS-encrypted session"); + } catch (const std::exception &e) { + LOG_ERROR("MQTT ERROR: %s", e.what()); + } + } else { + LOG_INFO("Using non-TLS-encrypted session"); + pubSub.setClient(mqttClient); + } +#else + pubSub.setClient(mqttClient); +#endif +#elif HAS_NETWORKING + pubSub.setClient(mqttClient); +#endif + + 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(); + } + pubSub.setServer(serverAddr, serverPort); + pubSub.setBufferSize(512); + + LOG_INFO("Attempting to connect directly to MQTT server %s, port: %d, username: %s, password: %s", serverAddr, serverPort, + mqttUsername, mqttPassword); + + bool connected = pubSub.connect(owner.id, mqttUsername, mqttPassword); + if (connected) { + LOG_INFO("MQTT connected"); + enabled = true; // Start running background process again + runASAP = true; + reconnectCount = 0; + + 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 + } +#endif + } +} + +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("Subscribing 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("Subscribing 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("Subscribing to %s", topic.c_str()); + pubSub.subscribe(topic.c_str(), 1); + } +#endif +#endif +} + +bool MQTT::wantsLink() const +{ + bool hasChannelorMapReport = + moduleConfig.mqtt.enabled && (moduleConfig.mqtt.map_reporting_enabled || channels.anyMqttEnabled()); + + if (hasChannelorMapReport && moduleConfig.mqtt.proxy_to_client_enabled) + return true; + +#if HAS_WIFI + return hasChannelorMapReport && WiFi.isConnected(); +#endif +#if HAS_ETHERNET + return hasChannelorMapReport && Ethernet.linkStatus() == LinkON; +#endif + return false; +} + +int32_t MQTT::runOnce() +{ +#if HAS_NETWORKING + if (!moduleConfig.mqtt.enabled || !(moduleConfig.mqtt.map_reporting_enabled || channels.anyMqttEnabled())) + return disable(); + + bool wantConnection = wantsLink(); + + 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; + } + + 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, dropping"); + pubSub.disconnect(); + } + + powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); // Suppress entering light sleep (because that would turn off bluetooth) + return 20; + } +#endif + return 30000; +} + +void MQTT::publishNodeInfo() +{ + // TODO: NodeInfo broadcast over MQTT only (NODENUM_BROADCAST_NO_LORA) +} +void MQTT::publishQueuedMessages() +{ + if (!mqttQueue.isEmpty()) { + LOG_DEBUG("Publishing enqueued MQTT message"); + meshtastic_ServiceEnvelope *env = mqttQueue.dequeuePtr(0); + size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, env); + std::string topic; + if (env->packet->pki_encrypted) { + topic = cryptTopic + "PKI/" + owner.id; + } else { + topic = cryptTopic + env->channel_id + "/" + owner.id; + } + LOG_INFO("publish %s, %u bytes from queue", 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) { + // handle json topic + auto jsonString = MeshPacketSerializer::JsonSerialize(env->packet); + if (jsonString.length() != 0) { + std::string topicJson; + if (env->packet->pki_encrypted) { + topicJson = jsonTopic + "PKI/" + owner.id; + } else { + topicJson = jsonTopic + env->channel_id + "/" + owner.id; + } + 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 + mqttPool.release(env); + } +} + +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) { + // 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 && mp_decoded.decoded.has_bitfield && + !(mp_decoded.decoded.bitfield & BITFIELD_OK_TO_MQTT_MASK) && + (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 (strcmp(moduleConfig.mqtt.address, default_mqtt_address) == 0 && + (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) || isPKIEncrypted) { + const char *channelId = isPKIEncrypted ? "PKI" : channels.getGlobalId(chIndex); + + meshtastic_ServiceEnvelope *env = mqttPool.allocZeroed(); + env->channel_id = (char *)channelId; + env->gateway_id = owner.id; + + LOG_DEBUG("MQTT onSend - Publishing "); + if (moduleConfig.mqtt.encryption_enabled) { + env->packet = (meshtastic_MeshPacket *)&mp_encrypted; + LOG_DEBUG("encrypted message"); + } else if (mp_decoded.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + env->packet = (meshtastic_MeshPacket *)&mp_decoded; + LOG_DEBUG("portnum %i message", env->packet->decoded.portnum); + } else { + LOG_DEBUG("nothing, pkt not decrypted"); + return; // Don't upload a still-encrypted PKI packet if not encryption_enabled + } + + if (moduleConfig.mqtt.proxy_to_client_enabled || this->isConnectedDirectly()) { + size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, env); + std::string topic = cryptTopic + channelId + "/" + owner.id; + 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) { + // handle json topic + auto jsonString = MeshPacketSerializer::JsonSerialize((meshtastic_MeshPacket *)&mp_decoded); + if (jsonString.length() != 0) { + std::string topicJson = jsonTopic + channelId + "/" + owner.id; + 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, queueing packet"); + if (mqttQueue.numFree() == 0) { + LOG_WARN("NOTE: MQTT queue is full, discarding oldest"); + meshtastic_ServiceEnvelope *d = mqttQueue.dequeuePtr(0); + if (d) + mqttPool.release(d); + } + // make a copy of serviceEnvelope and queue it + meshtastic_ServiceEnvelope *copied = mqttPool.allocCopy(*env); + assert(mqttQueue.enqueue(copied, 0)); + } + mqttPool.release(env); + } +} + +void MQTT::perhapsReportToMap() +{ + if (!moduleConfig.mqtt.map_reporting_enabled || !(moduleConfig.mqtt.proxy_to_client_enabled || isConnectedDirectly())) + return; + + if (Throttle::isWithinTimespanMs(last_report_to_map, map_publish_interval_msecs)) { + return; + } else { + if (map_position_precision == 0 || (localPosition.latitude_i == 0 && localPosition.longitude_i == 0)) { + last_report_to_map = millis(); + if (map_position_precision == 0) + LOG_WARN("MQTT Map reporting is enabled, but precision is 0"); + if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) + LOG_WARN("MQTT Map reporting is enabled, but no position available."); + return; + } + + // Allocate ServiceEnvelope and fill it + meshtastic_ServiceEnvelope *se = mqttPool.allocZeroed(); + se->channel_id = (char *)channels.getGlobalId(channels.getPrimaryIndex()); // Use primary channel as the channel_id + se->gateway_id = owner.id; + + // 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(); + + // Set position with precision (same as in PositionModule) + if (map_position_precision < 32 && map_position_precision > 0) { + 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)); + } else { + mapReport.latitude_i = localPosition.latitude_i; + mapReport.longitude_i = localPosition.longitude_i; + } + mapReport.altitude = localPosition.altitude; + mapReport.position_precision = map_position_precision; + + mapReport.num_online_local_nodes = nodeDB->getNumOnlineMeshNodes(true); + + // Encode MapReport message and set it to MeshPacket in ServiceEnvelope + mp->decoded.payload.size = pb_encode_to_bytes(mp->decoded.payload.bytes, sizeof(mp->decoded.payload.bytes), + &meshtastic_MapReport_msg, &mapReport); + se->packet = mp; + + 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 ServiceEnvelope and MeshPacket + mqttPool.release(se); + packetPool.release(mp); + + // Update the last report time + last_report_to_map = millis(); + } +} + +bool MQTT::isValidJsonEnvelope(JSONObject &json) +{ + // if "sender" is provided, avoid processing packets we uplinked + return (json.find("sender") != json.end() ? (json["sender"]->AsString().compare(owner.id) != 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 +} + +bool MQTT::isPrivateIpAddress(const char address[]) +{ + // Min. length like 10.0.0.0 (8), max like 192.168.255.255:65535 (21) + size_t length = strlen(address); + if (length < 8 || length > 21) { + return false; + } + + // Ensure the address contains only digits and dots and maybe a colon. + // Some limited validation is done. + // Even if it's not a valid IP address, we will know it's not a domain. + bool hasColon = false; + int numDots = 0; + for (size_t i = 0; i < length; i++) { + if (!isdigit(address[i]) && address[i] != '.' && address[i] != ':') { + return false; + } + + // Dots can't be the first character, immediately follow another dot, + // occur more than 3 times, or occur after a colon. + if (address[i] == '.') { + if (++numDots > 3 || i == 0 || address[i - 1] == '.' || hasColon) { + return false; + } + } + // There can only be a single colon, and it can only occur after 3 dots + else if (address[i] == ':') { + if (hasColon || numDots < 3) { + return false; + } + + hasColon = true; + } + } + + // Final validation for IPv4 address and port format. + // Note that the values of octets haven't been tested, only the address format. + if (numDots != 3) { + return false; + } + + // Check the easy ones first. + if (strcmp(address, "127.0.0.1") == 0 || strncmp(address, "10.", 3) == 0 || strncmp(address, "192.168", 7) == 0 || + strncmp(address, "169.254", 7) == 0) { + return true; + } + + // See if it's definitely not a 172 address. + if (strncmp(address, "172", 3) != 0) { + return false; + } + + // We know it's a 172 address, now see if the second octet is 2 digits. + if (address[6] != '.') { + return false; + } + + // Copy the second octet into a secondary buffer we can null-terminate and parse. + char octet2[3]; + strncpy(octet2, address + 4, 2); + octet2[2] = 0; + + int octet2Num = atoi(octet2); + return octet2Num >= 16 && octet2Num <= 31; +} \ No newline at end of file diff --git a/src/mqtt/MQTT.h b/src/mqtt/MQTT.h new file mode 100644 index 0000000..fcefb94 --- /dev/null +++ b/src/mqtt/MQTT.h @@ -0,0 +1,133 @@ +#pragma once + +#include "configuration.h" + +#include "concurrency/OSThread.h" +#include "mesh/Channels.h" +#include "mesh/generated/meshtastic/mqtt.pb.h" +#include "serialization/JSON.h" +#if HAS_WIFI +#include +#if !defined(ARCH_PORTDUINO) +#if defined(ESP_ARDUINO_VERSION_MAJOR) && ESP_ARDUINO_VERSION_MAJOR < 3 +#include +#endif +#endif +#endif +#if HAS_ETHERNET +#include +#endif + +#if HAS_NETWORKING +#include +#endif + +#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. + */ +class MQTT : private concurrency::OSThread +{ + // supposedly the current version is busted: + // http://www.iotsharing.com/2017/08/how-to-use-esp32-mqtts-with-mqtts-mosquitto-broker-tls-ssl.html +#if HAS_WIFI + WiFiClient mqttClient; +#if !defined(ARCH_PORTDUINO) +#if defined(ESP_ARDUINO_VERSION_MAJOR) && ESP_ARDUINO_VERSION_MAJOR < 3 + WiFiClientSecure wifiSecureClient; +#endif +#endif +#endif +#if HAS_ETHERNET + EthernetClient mqttClient; +#endif + + public: +#if HAS_NETWORKING + PubSubClient pubSub; +#endif + 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); + + /** Attempt to connect to server if necessary + */ + void reconnect(); + + bool isConnectedDirectly(); + + 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); + + void onClientProxyReceive(meshtastic_MqttClientProxyMessage msg); + + bool isEnabled() { return this->enabled; }; + + void start() { setIntervalFromNow(0); }; + + protected: + PointerQueue mqttQueue; + + int reconnectCount = 0; + + virtual int32_t runOnce() override; + + private: + 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 + const uint32_t default_map_publish_interval_secs = 60 * 15; // defaults to 15 minutes + 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; + + /** return true if we have a channel that wants uplink/downlink or map reporting is enabled + */ + bool wantsLink() const; + + /** 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); + + /// Called when a new publish arrives from the MQTT server + void onReceive(char *topic, byte *payload, size_t length); + + void publishQueuedMessages(); + + void publishNodeInfo(); + + // Check if we should report unencrypted information about our node for consumption by a map + void perhapsReportToMap(); + + // returns true if this is a valid JSON envelope which we accept on downlink + bool isValidJsonEnvelope(JSONObject &json); + + /// Determines if the given address is a private IPv4 address, i.e. not routable on the public internet. + /// These are the ranges: 127.0.0.1, 10.0.0.0-10.255.255.255, 172.16.0.0-172.31.255.255, 192.168.0.0-192.168.255.255. + bool isPrivateIpAddress(const char address[]); + + /// 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(); + +extern MQTT *mqtt; \ No newline at end of file diff --git a/src/network-stubs.cpp b/src/network-stubs.cpp new file mode 100644 index 0000000..1737579 --- /dev/null +++ b/src/network-stubs.cpp @@ -0,0 +1,31 @@ +#include "configuration.h" + +#if (HAS_WIFI == 0) + +bool initWifi() +{ + return false; +} + +void deinitWifi() {} + +bool isWifiAvailable() +{ + return false; +} + +#endif + +#if (HAS_ETHERNET == 0) + +bool initEthernet() +{ + return false; +} + +bool isEthernetAvailable() +{ + return false; +} + +#endif diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp new file mode 100644 index 0000000..933d05d --- /dev/null +++ b/src/nimble/NimbleBluetooth.cpp @@ -0,0 +1,301 @@ +#include "configuration.h" +#if !MESHTASTIC_EXCLUDE_BLUETOOTH +#include "BluetoothCommon.h" +#include "NimbleBluetooth.h" +#include "PowerFSM.h" + +#include "main.h" +#include "mesh/PhoneAPI.h" +#include "mesh/mesh-pb-constants.h" +#include "sleep.h" +#include + +NimBLECharacteristic *fromNumCharacteristic; +NimBLECharacteristic *BatteryCharacteristic; +NimBLECharacteristic *logRadioCharacteristic; +NimBLEServer *bleServer; + +static bool passkeyShowing; + +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) + { + PhoneAPI::onNowHasData(fromRadioNum); + + LOG_INFO("BLE notify fromNum"); + + uint8_t val[4]; + put_le32(val, fromRadioNum); + + fromNumCharacteristic->setValue(val, sizeof(val)); + fromNumCharacteristic->notify(); + } + + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() { return bleServer && bleServer->getConnectedCount() > 0; } +}; + +static BluetoothPhoneAPI *bluetoothPhoneAPI; +/** + * Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies) + */ + +// Last ToRadio value received from the phone +static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE]; + +class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks +{ + virtual void onWrite(NimBLECharacteristic *pCharacteristic) + { + LOG_INFO("To Radio onwrite"); + auto val = pCharacteristic->getValue(); + + if (memcmp(lastToRadio, val.data(), val.length()) != 0) { + LOG_DEBUG("New ToRadio packet"); + memcpy(lastToRadio, val.data(), val.length()); + bluetoothPhoneAPI->handleToRadio(val.data(), val.length()); + } else { + LOG_DEBUG("Dropping duplicate ToRadio packet we just saw"); + } + } +}; + +class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks +{ + virtual void onRead(NimBLECharacteristic *pCharacteristic) + { + uint8_t fromRadioBytes[meshtastic_FromRadio_size]; + size_t numBytes = bluetoothPhoneAPI->getFromRadio(fromRadioBytes); + + std::string fromRadioByteString(fromRadioBytes, fromRadioBytes + numBytes); + + pCharacteristic->setValue(fromRadioByteString); + } +}; + +class NimbleBluetoothServerCallback : public NimBLEServerCallbacks +{ + virtual uint32_t onPassKeyRequest() + { + uint32_t passkey = config.bluetooth.fixed_pin; + + if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN) { + LOG_INFO("Using 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); +#if HAS_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 : 32; + 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_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 + passkeyShowing = true; + + return passkey; + } + + virtual void onAuthenticationComplete(ble_gap_conn_desc *desc) + { + LOG_INFO("BLE authentication complete"); + + if (passkeyShowing) { + passkeyShowing = false; + screen->endAlert(); + } + } + + virtual void onDisconnect(NimBLEServer *pServer, ble_gap_conn_desc *desc) + { + LOG_INFO("BLE disconnect"); + + if (bluetoothPhoneAPI) { + bluetoothPhoneAPI->close(); + } + } +}; + +static NimbleBluetoothToRadioCallback *toRadioCallbacks; +static NimbleBluetoothFromRadioCallback *fromRadioCallbacks; + +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(); +#endif +} + +// Proper shutdown for ESP32. Needs reboot to reverse. +void NimbleBluetooth::deinit() +{ +#ifdef ARCH_ESP32 + LOG_INFO("Disable bluetooth until reboot"); + NimBLEDevice::deinit(); +#endif +} + +// Has initial setup been completed +bool NimbleBluetooth::isActive() +{ + return bleServer; +} + +bool NimbleBluetooth::isConnected() +{ + return bleServer->getConnectedCount() > 0; +} + +int NimbleBluetooth::getRssi() +{ + if (bleServer && isConnected()) { + auto service = bleServer->getServiceByUUID(MESH_SERVICE_UUID); + uint16_t handle = service->getHandle(); + return NimBLEDevice::getClientByID(handle)->getRssi(); + } + return 0; // FIXME figure out where to source this +} + +void NimbleBluetooth::setup() +{ + // Uncomment for testing + // NimbleBluetooth::clearBonds(); + + LOG_INFO("Initialise the NimBLE bluetooth module"); + + NimBLEDevice::init(getDeviceName()); + NimBLEDevice::setPower(ESP_PWR_LVL_P9); + + 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(); + + NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback(); + 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); + 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); + + fromRadioCallbacks = new NimbleBluetoothFromRadioCallback(); + FromRadioCharacteristic->setCallbacks(fromRadioCallbacks); + + 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); + + NimBLE2904 *batteryLevelDescriptor = (NimBLE2904 *)BatteryCharacteristic->createDescriptor((uint16_t)0x2904); + batteryLevelDescriptor->setFormat(NimBLE2904::FORMAT_UINT8); + batteryLevelDescriptor->setNamespace(1); + batteryLevelDescriptor->setUnit(0x27ad); + + batteryService->start(); +} + +void NimbleBluetooth::startAdvertising() +{ + 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); +} + +/// 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); + BatteryCharacteristic->notify(); + } +} + +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; + } + logRadioCharacteristic->notify(logMessage, length, true); +} + +void clearNVS() +{ + NimBLEDevice::deleteAllBonds(); +#ifdef ARCH_ESP32 + ESP.restart(); +#endif +} +#endif \ No newline at end of file diff --git a/src/nimble/NimbleBluetooth.h b/src/nimble/NimbleBluetooth.h new file mode 100644 index 0000000..45602e0 --- /dev/null +++ b/src/nimble/NimbleBluetooth.h @@ -0,0 +1,22 @@ +#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); + + private: + void setupService(); + void startAdvertising(); +}; + +void setBluetoothEnable(bool enable); +void clearNVS(); \ No newline at end of file diff --git a/src/platform/esp32/BleOta.cpp b/src/platform/esp32/BleOta.cpp new file mode 100644 index 0000000..698336f --- /dev/null +++ b/src/platform/esp32/BleOta.cpp @@ -0,0 +1,46 @@ +#include "BleOta.h" +#include "Arduino.h" +#include + +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); + + 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) { + 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; +} + +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 new file mode 100644 index 0000000..f4c5109 --- /dev/null +++ b/src/platform/esp32/BleOta.h @@ -0,0 +1,20 @@ +#ifndef BLEOTA_H +#define BLEOTA_H + +#include +#include + +class BleOta +{ + public: + explicit BleOta(){}; + + static String getOtaAppVersion(); + static bool switchToOtaApp(); + + 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 new file mode 100644 index 0000000..b554a3d --- /dev/null +++ b/src/platform/esp32/ESP32CryptoEngine.cpp @@ -0,0 +1,41 @@ +#include "CryptoEngine.h" +#include "configuration.h" + +#include "mbedtls/aes.h" + +class ESP32CryptoEngine : public CryptoEngine +{ + + mbedtls_aes_context aes; + + public: + ESP32CryptoEngine() { mbedtls_aes_init(&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); + } + } + } +}; + +CryptoEngine *crypto = new ESP32CryptoEngine(); \ No newline at end of file diff --git a/src/platform/esp32/SimpleAllocator.cpp b/src/platform/esp32/SimpleAllocator.cpp new file mode 100644 index 0000000..d482a3f --- /dev/null +++ b/src/platform/esp32/SimpleAllocator.cpp @@ -0,0 +1,28 @@ +#include "SimpleAllocator.h" +#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; +} + +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 new file mode 100644 index 0000000..eaf4ee7 --- /dev/null +++ b/src/platform/esp32/SimpleAllocator.h @@ -0,0 +1,43 @@ +#pragma once +#include + +#define POOL_SIZE 16384 + +/** + * An allocator (and placement new operator) that allocates storage from a fixed sized buffer. + * It will panic if that buffer fills up. + * If you are _sure_ no outstanding references to blocks in this buffer still exist, you can call + * reset() to start from scratch. + * + * 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] = {}; + + uint32_t nextFree = 0; + + public: + SimpleAllocator(); + + 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(); +}; + +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(); +}; diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h new file mode 100644 index 0000000..ba3050e --- /dev/null +++ b/src/platform/esp32/architecture.h @@ -0,0 +1,201 @@ +#pragma once + +#define ARCH_ESP32 + +// +// defaults for ESP32 architecture +// + +#ifndef HAS_BLUETOOTH +#define HAS_BLUETOOTH 1 +#endif +#ifndef HAS_WIFI +#define HAS_WIFI 1 +#endif +#ifndef HAS_SCREEN +#define HAS_SCREEN 1 +#endif +#ifndef HAS_WIRE +#define HAS_WIRE 1 +#endif +#ifndef HAS_GPS +#define HAS_GPS 1 +#endif +#ifndef HAS_BUTTON +#define HAS_BUTTON 1 +#endif +#ifndef HAS_TELEMETRY +#define HAS_TELEMETRY 1 +#endif +#ifndef HAS_SENSOR +#define HAS_SENSOR 1 +#endif +#ifndef HAS_RADIO +#define HAS_RADIO 1 +#endif +#ifndef HAS_RTC +#define HAS_RTC 1 +#endif +#ifndef HAS_CPU_SHUTDOWN +#define HAS_CPU_SHUTDOWN 1 +#endif +#ifndef DEFAULT_VREF +#define DEFAULT_VREF 1100 +#endif +#ifndef HAS_CUSTOM_CRYPTO_ENGINE +#define HAS_CUSTOM_CRYPTO_ENGINE 1 +#endif + +#if defined(HAS_AXP192) || defined(HAS_AXP2101) +#define HAS_PMU +#endif + +#ifdef PIN_BUTTON_TOUCH +#define BUTTON_PIN_TOUCH PIN_BUTTON_TOUCH +#endif +// +// set HW_VENDOR +// + +// This string must exactly match the case used in release file names or the android updater won't work + +#if defined(TBEAM_V10) +#define HW_VENDOR meshtastic_HardwareModel_TBEAM +#elif defined(TBEAM_V07) +#define HW_VENDOR meshtastic_HardwareModel_TBEAM_V0P7 +#elif defined(LILYGO_TBEAM_S3_CORE) +#define HW_VENDOR meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE +#elif defined(DIY_V1) +#define HW_VENDOR meshtastic_HardwareModel_DIY_V1 +#elif defined(RAK_11200) +#define HW_VENDOR meshtastic_HardwareModel_RAK11200 +#elif defined(ARDUINO_HELTEC_WIFI_LORA_32_V2) +#ifdef HELTEC_V2_0 +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_V2_0 +#endif +#ifdef HELTEC_V2_1 +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_V2_1 +#endif +#elif defined(HELTEC_WIRELESS_BRIDGE) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_WIRELESS_BRIDGE +#elif defined(ARDUINO_HELTEC_WIFI_LORA_32) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_V1 +#elif defined(TLORA_V1) +#define HW_VENDOR meshtastic_HardwareModel_TLORA_V1 +#elif defined(TLORA_V2) +#define HW_VENDOR meshtastic_HardwareModel_TLORA_V2 +#elif defined(TLORA_V1_3) +#define HW_VENDOR meshtastic_HardwareModel_TLORA_V1_1P3 +#elif defined(TLORA_V2_1_16) +#define HW_VENDOR meshtastic_HardwareModel_TLORA_V2_1_1P6 +#elif defined(TLORA_V2_1_18) +#define HW_VENDOR meshtastic_HardwareModel_TLORA_V2_1_1P8 +#elif defined(TLORA_C6) +#define HW_VENDOR meshtastic_HardwareModel_TLORA_C6 +#elif defined(T_DECK) +#define HW_VENDOR meshtastic_HardwareModel_T_DECK +#elif defined(T_WATCH_S3) +#define HW_VENDOR meshtastic_HardwareModel_T_WATCH_S3 +#elif defined(GENIEBLOCKS) +#define HW_VENDOR meshtastic_HardwareModel_GENIEBLOCKS +#elif defined(PRIVATE_HW) +#define HW_VENDOR meshtastic_HardwareModel_PRIVATE_HW +#elif defined(NANO_G1) +#define HW_VENDOR meshtastic_HardwareModel_NANO_G1 +#elif defined(M5STACK) +#define HW_VENDOR meshtastic_HardwareModel_M5STACK +#elif defined(M5STACK_CORES3) +#define HW_VENDOR meshtastic_HardwareModel_M5STACK_CORES3 +#elif defined(STATION_G1) +#define HW_VENDOR meshtastic_HardwareModel_STATION_G1 +#elif defined(DR_DEV) +#define HW_VENDOR meshtastic_HardwareModel_DR_DEV +#elif defined(HELTEC_HRU_3601) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_HRU_3601 +#elif defined(HELTEC_V3) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_V3 +#elif defined(HELTEC_WSL_V3) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_WSL_V3 +#elif defined(HELTEC_WIRELESS_TRACKER) +#ifdef HELTEC_TRACKER_V1_0 +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_WIRELESS_TRACKER_V1_0 +#else +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_WIRELESS_TRACKER +#endif +#elif defined(HELTEC_WIRELESS_PAPER_V1_0) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_WIRELESS_PAPER_V1_0 +#elif defined(HELTEC_WIRELESS_PAPER) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_WIRELESS_PAPER +#elif defined(TLORA_T3S3_V1) +#define HW_VENDOR meshtastic_HardwareModel_TLORA_T3_S3 +#elif defined(TLORA_T3S3_EPAPER) +#define HW_VENDOR meshtastic_HardwareModel_TLORA_T3_S3 +#elif defined(CDEBYTE_EORA_S3) +#define HW_VENDOR meshtastic_HardwareModel_CDEBYTE_EORA_S3 +#elif defined(BETAFPV_2400_TX) +#define HW_VENDOR meshtastic_HardwareModel_BETAFPV_2400_TX +#elif defined(NANO_G1_EXPLORER) +#define HW_VENDOR meshtastic_HardwareModel_NANO_G1_EXPLORER +#elif defined(BETAFPV_900_TX_NANO) +#define HW_VENDOR meshtastic_HardwareModel_BETAFPV_900_NANO_TX +#elif defined(PICOMPUTER_S3) +#define HW_VENDOR meshtastic_HardwareModel_PICOMPUTER_S3 +#elif defined(HELTEC_HT62) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_HT62 +#elif defined(EBYTE_ESP32_S3) +#define HW_VENDOR meshtastic_HardwareModel_EBYTE_ESP32_S3 +#elif defined(ESP32_S3_PICO) +#define HW_VENDOR meshtastic_HardwareModel_ESP32_S3_PICO +#elif defined(SENSELORA_S3) +#define HW_VENDOR meshtastic_HardwareModel_SENSELORA_S3 +#elif defined(HELTEC_HT62) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_HT62 +#elif defined(CHATTER_2) +#define HW_VENDOR meshtastic_HardwareModel_CHATTER_2 +#elif defined(STATION_G2) +#define HW_VENDOR meshtastic_HardwareModel_STATION_G2 +#elif defined(UNPHONE) +#define HW_VENDOR meshtastic_HardwareModel_UNPHONE +#elif defined(WIPHONE) +#define HW_VENDOR meshtastic_HardwareModel_WIPHONE +#elif defined(RADIOMASTER_900_BANDIT_NANO) +#define HW_VENDOR meshtastic_HardwareModel_RADIOMASTER_900_BANDIT_NANO +#elif defined(RADIOMASTER_900_BANDIT) +#define HW_VENDOR meshtastic_HardwareModel_RADIOMASTER_900_BANDIT +#elif defined(HELTEC_CAPSULE_SENSOR_V3) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_CAPSULE_SENSOR_V3 +#elif defined(HELTEC_VISION_MASTER_T190) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_VISION_MASTER_T190 +#elif defined(HELTEC_VISION_MASTER_E213) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_VISION_MASTER_E213 +#elif defined(HELTEC_VISION_MASTER_E290) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_VISION_MASTER_E290 +#elif defined(HELTEC_MESH_NODE_T114) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_MESH_NODE_T114 +#elif defined(SENSECAP_INDICATOR) +#define HW_VENDOR meshtastic_HardwareModel_SENSECAP_INDICATOR +#elif defined(SEEED_XIAO_S3) +#define HW_VENDOR meshtastic_HardwareModel_SEEED_XIAO_S3 +#endif + +// ----------------------------------------------------------------------------- +// LoRa SPI +// ----------------------------------------------------------------------------- + +// 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. +#ifndef LORA_SCK +#define LORA_SCK 5 +#endif +#ifndef LORA_MISO +#define LORA_MISO 19 +#endif +#ifndef LORA_MOSI +#define LORA_MOSI 27 +#endif +#ifndef LORA_CS +#define LORA_CS 18 +#endif + +#define SERIAL0_RX_GPIO 3 // Always GPIO3 on ESP32 // FIXME: may be different on ESP32-S3, etc. diff --git a/src/platform/esp32/iram-quirk.c b/src/platform/esp32/iram-quirk.c new file mode 100644 index 0000000..8138421 --- /dev/null +++ b/src/platform/esp32/iram-quirk.c @@ -0,0 +1,23 @@ +// Free up some precious space in the iram0_0_seg memory segment + +#include + +#include +#include +#include + +#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; +} + +const spi_flash_chip_t stub_flash_chip __attribute__((IRAM_SECTION)) = { + .name = "stub", + .probe = stub_probe, +}; + +extern const spi_flash_chip_t __wrap_esp_flash_chip_gd __attribute__((IRAM_SECTION, alias("stub_flash_chip"))); +extern const spi_flash_chip_t __wrap_esp_flash_chip_issi __attribute__((IRAM_SECTION, alias("stub_flash_chip"))); +extern const spi_flash_chip_t __wrap_esp_flash_chip_winbond __attribute__((IRAM_SECTION, alias("stub_flash_chip"))); diff --git a/src/platform/esp32/main-esp32.cpp b/src/platform/esp32/main-esp32.cpp new file mode 100644 index 0000000..7b3493f --- /dev/null +++ b/src/platform/esp32/main-esp32.cpp @@ -0,0 +1,246 @@ +#include "PowerFSM.h" +#include "PowerMon.h" +#include "configuration.h" +#include "esp_task_wdt.h" +#include "main.h" + +#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_BLUETOOTH +#include "BleOta.h" +#include "nimble/NimbleBluetooth.h" +#endif + +#if HAS_WIFI +#include "mesh/wifi/WiFiAPClient.h" +#endif + +#include "esp_mac.h" +#include "meshUtils.h" +#include "sleep.h" +#include "soc/rtc.h" +#include "target_specific.h" +#include +#include +#include +#include + +#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_BLUETOOTH +void setBluetoothEnable(bool enable) +{ +#if HAS_WIFI + if (!isWifiAvailable() && config.bluetooth.enabled == true) +#else + 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 + } +} +#else +void setBluetoothEnable(bool enable) {} +void updateBatteryLevel(uint8_t level) {} +#endif + +void getMacAddr(uint8_t *dmac) +{ + assert(esp_efuse_mac_get_default(dmac) == ESP_OK); +} + +#ifdef 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; +} + +void enableSlowCLK() +{ + rtc_clk_32k_enable(true); + + 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("Switching 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); + 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("Setting random seed %u", seed); + randomSeed(seed); + */ + + 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); + + LOG_DEBUG("Setup Preferences in Flash Storage"); + + // 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); +#if !MESHTASTIC_EXCLUDE_BLUETOOTH + String BLEOTA = BleOta::getOtaAppVersion(); + if (BLEOTA.isEmpty()) { + LOG_INFO("No OTA firmware available"); + } else { + LOG_INFO("OTA firmware version %s", BLEOTA.c_str()); + } +#else + LOG_INFO("No OTA firmware available"); +#endif + + // 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. +// #define APP_WATCHDOG_SECS 45 +#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); +#else + res = esp_task_wdt_init(APP_WATCHDOG_SECS, true); + assert(res == ESP_OK); +#endif + res = esp_task_wdt_add(NULL); + assert(res == ESP_OK); + +#ifdef HAS_32768HZ + enableSlowCLK(); +#endif +} + +/// loop code specific to ESP32 targets +void esp32Loop() +{ + esp_task_wdt_reset(); // service our app level watchdog + + // 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. + + 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... + */ +#if SOC_RTCIO_HOLD_SUPPORTED + 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, +#endif +#ifndef USE_JTAG + 13, +#endif + 34, 35, 37}; + + 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. +#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. +#if SOC_RTCIO_HOLD_SUPPORTED && SOC_PM_SUPPORT_EXT_WAKEUP + 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); +#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); + +#ifdef 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); +#else + 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); + + 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/README.md b/src/platform/extra_variants/README.md new file mode 100644 index 0000000..e558502 --- /dev/null +++ b/src/platform/extra_variants/README.md @@ -0,0 +1,15 @@ +# About extra_variants + +This directory tree is designed to solve two problems. + +- The ESP32 arduino/platformio project doesn't support the nice "if initVariant() is found, call that after init" behavior of the nrf52 builds (they use initVariant() internally). +- Over the years a lot of 'board specific' init code has been added to init() in main.cpp. It would be great to have a general/clean mechanism to allow developers to specify board specific/unique code in a clean fashion without mucking in main. + +So we are borrowing the initVariant() ideas here (by using weak gcc references). You can now define lateInitVariant() if your board needs it. + +If you'd like a board specific variant to be run, add the variant.cpp file to an appropriately named +subdirectory and check for \_VARIANT_boardname in the cpp file (so that your code is only built for your board). +You'll need to define \_VARIANT_boardname in your corresponding variant.h file. +See existing boards for examples. + +This approach has no added runtime cost. diff --git a/src/platform/extra_variants/heltec_wireless_tracker/variant.cpp b/src/platform/extra_variants/heltec_wireless_tracker/variant.cpp new file mode 100644 index 0000000..12960e2 --- /dev/null +++ b/src/platform/extra_variants/heltec_wireless_tracker/variant.cpp @@ -0,0 +1,41 @@ +#include "configuration.h" + +#ifdef _VARIANT_HELTEC_WIRELESS_TRACKER + +#include "GPS.h" +#include "GpioLogic.h" +#include "graphics/TFTDisplay.h" + +// Heltec tracker specific init +void lateInitVariant() +{ + // LOG_DEBUG("Heltec tracker initVariant"); + +#ifndef MESHTASTIC_EXCLUDE_GPS + GpioVirtPin *virtGpsEnable = gps ? gps->enablePin : new GpioVirtPin(); +#else + 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; + + // 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); +#endif +} + +#endif \ No newline at end of file diff --git a/src/platform/nrf52/BLEDfuScure.cpp b/src/platform/nrf52/BLEDfuScure.cpp new file mode 100644 index 0000000..82cb890 --- /dev/null +++ b/src/platform/nrf52/BLEDfuScure.cpp @@ -0,0 +1,143 @@ +/**************************************************************************/ +/*! + @file BLEDfu.cpp + @author hathach (tinyusb.org) + + @section LICENSE + + Software License Agreement (BSD License) + + Copyright (c) 2018, Adafruit Industries (adafruit.com) + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the copyright holders nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +/**************************************************************************/ + +#include "BLEDfuSecure.h" +#include "bluefruit.h" + +#define DFU_REV_APPMODE 0x0001 + +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}; + +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; + + 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); + + 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(); + } + } +} + +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()); + + _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; +} diff --git a/src/platform/nrf52/BLEDfuSecure.h b/src/platform/nrf52/BLEDfuSecure.h new file mode 100644 index 0000000..bd5d910 --- /dev/null +++ b/src/platform/nrf52/BLEDfuSecure.h @@ -0,0 +1,55 @@ +/**************************************************************************/ +/*! + @file BLEDfu.h + @author hathach (tinyusb.org) + + @section LICENSE + + Software License Agreement (BSD License) + + Copyright (c) 2018, Adafruit Industries (adafruit.com) + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the copyright holders nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +/**************************************************************************/ +#ifndef BLEDFUSECURE_H_ +#define BLEDFUSECURE_H_ + +#include "bluefruit_common.h" + +#include "BLECharacteristic.h" +#include "BLEService.h" + +class BLEDfuSecure : public BLEService +{ + protected: + BLECharacteristic _chr_control; + + public: + BLEDfuSecure(void); + + virtual err_t begin(void); +}; + +#endif /* BLEDFUSECURE_H_ */ diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp new file mode 100644 index 0000000..9326248 --- /dev/null +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -0,0 +1,377 @@ +#include "NRF52Bluetooth.h" +#include "BLEDfuSecure.h" +#include "BluetoothCommon.h" +#include "PowerFSM.h" +#include "configuration.h" +#include "main.h" +#include "mesh/PhoneAPI.h" +#include "mesh/mesh-pb-constants.h" +#include +#include +static BLEService meshBleService = BLEService(BLEUuid(MESH_SERVICE_UUID_16)); +static BLECharacteristic fromNum = BLECharacteristic(BLEUuid(FROMNUM_UUID_16)); +static BLECharacteristic fromRadio = BLECharacteristic(BLEUuid(FROMRADIO_UUID_16)); +static BLECharacteristic toRadio = BLECharacteristic(BLEUuid(TORADIO_UUID_16)); +static BLECharacteristic logRadio = BLECharacteristic(BLEUuid(LOGRADIO_UUID_16)); + +static BLEDis bledis; // DIS (Device Information Service) helper class instance +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 +#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)]; +static uint8_t fromRadioBytes[meshtastic_FromRadio_size]; +static uint8_t toRadioBytes[meshtastic_ToRadio_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); + + 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 + { + BLEConnection *connection = Bluefruit.Connection(connectionHandle); + return connection->connected(); + } +}; + +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); +} +/** + * 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 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); +} +/** + * 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("Ignoring successor read"); + } + authorizeRead(conn_hdl); +} +// Last ToRadio value received from the phone +static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE]; + +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("Dropping duplicate 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(); + + 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(); + + 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"); + uint8_t connection_num = Bluefruit.connected(); + if (connection_num) { + for (uint8_t i = 0; i < connection_num; i++) { + LOG_INFO("NRF52 bluetooth disconnecting handle %d", i); + Bluefruit.disconnect(i); + } + delay(100); // wait for ondisconnect; + } + 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("Disabling NRF52 Bluetooth. (Workaround: tx power min, advertising stopped)"); +} +bool NRF52Bluetooth::isConnected() +{ + return Bluefruit.connected(connectionHandle); +} +int NRF52Bluetooth::getRssi() +{ + return 0; // FIXME figure out where to source this +} +void NRF52Bluetooth::setup() +{ + // Initialise the Bluefruit module + LOG_INFO("Initialize 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 +#else + 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("Configuring 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("Configuring 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("Configuring the Mesh bluetooth service"); + setupMeshService(); + // Setup the advertising packet(s) + LOG_INFO("Setting up the advertising payload(s)"); + startAdv(); + LOG_INFO("Advertising"); +} +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("Clearing 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) +{ + LOG_INFO("BLE pairing process started with passkey %.3s %.3s", passkey, passkey + 3); + powerFSM.trigger(EVENT_BLUETOOTH_PAIR); +#if !defined(MESHTASTIC_EXCLUDE_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 : 32; + 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_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; + } + } + LOG_INFO("BLE passkey pairing: match_request=%i", match_request); + return true; +} +void NRF52Bluetooth::onPairingCompleted(uint16_t conn_handle, uint8_t auth_status) +{ + if (auth_status == BLE_GAP_SEC_STATUS_SUCCESS) + LOG_INFO("BLE pairing success"); + else + LOG_INFO("BLE pairing failed"); + 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); +} \ No newline at end of file diff --git a/src/platform/nrf52/NRF52Bluetooth.h b/src/platform/nrf52/NRF52Bluetooth.h new file mode 100644 index 0000000..2229163 --- /dev/null +++ b/src/platform/nrf52/NRF52Bluetooth.h @@ -0,0 +1,22 @@ +#pragma once + +#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); + + 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); +}; \ No newline at end of file diff --git a/src/platform/nrf52/NRF52CryptoEngine.cpp b/src/platform/nrf52/NRF52CryptoEngine.cpp new file mode 100644 index 0000000..5de13c5 --- /dev/null +++ b/src/platform/nrf52/NRF52CryptoEngine.cpp @@ -0,0 +1,32 @@ +#include "CryptoEngine.h" +#include "aes-256/tiny-aes.h" +#include "configuration.h" +#include +class NRF52CryptoEngine : public CryptoEngine +{ + public: + 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); + } + } +}; + +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 new file mode 100644 index 0000000..20a7344 --- /dev/null +++ b/src/platform/nrf52/aes-256/tiny-aes.cpp @@ -0,0 +1,216 @@ +/* +AES-256 Software Implementation + +based on https://github.com/kokke/tiny-AES-C/ which is in public domain + +NOTE: String length must be evenly divisible by 16byte (str_len % 16 == 0) + You should pad the end of the string with zeros if this is not the case. + For AES192/256 the key size is proportionally larger. +*/ + +#include "tiny-aes.h" +#include + +#define Nb 4 +#define Nk 8 +#define Nr 14 + +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, + 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]; + + 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]; + } + + 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 new file mode 100644 index 0000000..ef78e0a --- /dev/null +++ b/src/platform/nrf52/aes-256/tiny-aes.h @@ -0,0 +1,22 @@ +#ifndef _TINY_AES_H_ +#define _TINY_AES_H_ + +#include +#include + +#define AES_BLOCKLEN 16 // Block length in bytes - AES is 128b block only +// #define AES_KEYLEN 32 +#define AES_keyExpSize 240 + +struct AES_ctx { + uint8_t RoundKey[AES_keyExpSize]; + uint8_t Iv[AES_BLOCKLEN]; +}; + +void AES_init_ctx(struct AES_ctx *ctx, const uint8_t *key); +void AES_init_ctx_iv(struct AES_ctx *ctx, const uint8_t *key, const uint8_t *iv); +void AES_ctx_set_iv(struct AES_ctx *ctx, const uint8_t *iv); + +void AES_CTR_xcrypt_buffer(struct AES_ctx *ctx, uint8_t *buf, size_t length); + +#endif // _TINY_AES_H_ diff --git a/src/platform/nrf52/alloc.cpp b/src/platform/nrf52/alloc.cpp new file mode 100644 index 0000000..0a610bb --- /dev/null +++ b/src/platform/nrf52/alloc.cpp @@ -0,0 +1,32 @@ +#include "configuration.h" +#include "rtos.h" +#include +#include + +/** + * 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 delete(void *ptr) +{ + rtos_free(ptr); +} + +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 new file mode 100644 index 0000000..b2b7b5a --- /dev/null +++ b/src/platform/nrf52/architecture.h @@ -0,0 +1,128 @@ +#pragma once + +#define ARCH_NRF52 + +// +// defaults for NRF52 architecture +// +#ifndef HAS_BLUETOOTH +#define HAS_BLUETOOTH 1 +#endif +#ifndef HAS_SCREEN +#define HAS_SCREEN 1 +#endif +#ifndef HAS_WIRE +#define HAS_WIRE 1 +#endif +#ifndef HAS_GPS +#define HAS_GPS 1 +#endif +#ifndef HAS_BUTTON +#define HAS_BUTTON 1 +#endif +#ifndef HAS_TELEMETRY +#define HAS_TELEMETRY 1 +#endif +#ifndef HAS_SENSOR +#define HAS_SENSOR 1 +#endif +#ifndef HAS_RADIO +#define HAS_RADIO 1 +#endif +#ifndef HAS_CPU_SHUTDOWN +#define HAS_CPU_SHUTDOWN 1 +#endif +#ifndef HAS_CUSTOM_CRYPTO_ENGINE +#define HAS_CUSTOM_CRYPTO_ENGINE 1 +#endif + +// +// set HW_VENDOR +// + +// This string must exactly match the case used in release file names or the android updater won't work +#ifdef ARDUINO_NRF52840_PCA10056 +#define HW_VENDOR meshtastic_HardwareModel_NRF52840DK +#elif defined(ARDUINO_NRF52840_PPR) +#define HW_VENDOR meshtastic_HardwareModel_PPR +#elif defined(RAK2560) +#define HW_VENDOR meshtastic_HardwareModel_RAK2560 +#elif defined(RAK4630) +#define HW_VENDOR meshtastic_HardwareModel_RAK4631 +#elif defined(TTGO_T_ECHO) +#define HW_VENDOR meshtastic_HardwareModel_T_ECHO +#elif defined(NANO_G2_ULTRA) +#define HW_VENDOR meshtastic_HardwareModel_NANO_G2_ULTRA +#elif defined(CANARYONE) +#define HW_VENDOR meshtastic_HardwareModel_CANARYONE +#elif defined(NORDIC_PCA10059) +#define HW_VENDOR meshtastic_HardwareModel_NRF52840_PCA10059 +#elif defined(TWC_MESH_V4) +#define HW_VENDOR meshtastic_HardwareModel_TWC_MESH_V4 +#elif defined(NRF52_PROMICRO_DIY) +#define HW_VENDOR meshtastic_HardwareModel_NRF52_PROMICRO_DIY +#elif defined(WIO_WM1110) +#define HW_VENDOR meshtastic_HardwareModel_WIO_WM1110 +#elif defined(TRACKER_T1000_E) +#define HW_VENDOR meshtastic_HardwareModel_TRACKER_T1000_E +#elif defined(ME25LS01_4Y10TD) +#define HW_VENDOR meshtastic_HardwareModel_ME25LS01_4Y10TD +#elif defined(MS24SF1) +#define HW_VENDOR meshtastic_HardwareModel_MS24SF1 +#elif defined(PRIVATE_HW) || defined(FEATHER_DIY) +#define HW_VENDOR meshtastic_HardwareModel_PRIVATE_HW +#elif defined(HELTEC_T114) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_MESH_NODE_T114 +#else +#define HW_VENDOR meshtastic_HardwareModel_NRF52_UNKNOWN +#endif + +// +// Standard definitions for NRF52 targets +// + +#ifdef ARDUINO_NRF52840_PCA10056 + +// This board uses 0 to be mean LED on +#undef LED_STATE_ON +#define LED_STATE_ON 0 // State when LED is lit + +#endif + +#ifdef _SEEED_XIAO_NRF52840_SENSE_H_ + +// This board uses 0 to be mean LED on +#undef LED_STATE_ON +#define LED_STATE_ON 0 // State when LED is lit + +#endif + +#define LED_PIN PIN_LED1 // LED1 on nrf52840-DK + +#ifdef PIN_BUTTON1 +#define BUTTON_PIN PIN_BUTTON1 +#endif + +#ifdef PIN_BUTTON2 +#define BUTTON_PIN_ALT PIN_BUTTON2 +#endif + +#ifdef PIN_BUTTON_TOUCH +#define BUTTON_PIN_TOUCH PIN_BUTTON_TOUCH +#endif + +// Always include the SEGGER code on NRF52 - because useful for debugging +#include "SEGGER_RTT.h" + +// The channel we send stdout data to +#define SEGGER_STDOUT_CH 0 + +// 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 !defined(PIN_SERIAL_RX) && !defined(NRF52840_XXAA) +// No serial ports on this board - ONLY use segger in memory console +#define USE_SEGGER +#endif diff --git a/src/platform/nrf52/hardfault.cpp b/src/platform/nrf52/hardfault.cpp new file mode 100644 index 0000000..7b7a718 --- /dev/null +++ b/src/platform/nrf52/hardfault.cpp @@ -0,0 +1,112 @@ +#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/ + +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 +#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 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"); +} + +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->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("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"); +} +#elif !defined(ARCH_NRF52) + +/* The prototype shows it is a naked function - in effect this is just an +assembly function. */ +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"); +} +#endif diff --git a/src/platform/nrf52/main-bare.cpp b/src/platform/nrf52/main-bare.cpp new file mode 100644 index 0000000..0bba6d6 --- /dev/null +++ b/src/platform/nrf52/main-bare.cpp @@ -0,0 +1 @@ +#include "target_specific.h" diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp new file mode 100644 index 0000000..72a223c --- /dev/null +++ b/src/platform/nrf52/main-nrf52.cpp @@ -0,0 +1,312 @@ +#include "configuration.h" +#include +#include +#include +#include +#include +#include +#include +#include +// #include +#include "NodeDB.h" +#include "PowerMon.h" +#include "error.h" +#include "main.h" +#include "meshUtils.h" + +#ifdef BQ25703A_ADDR +#include "BQ25713.h" +#endif + +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()); +} + +// 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 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; + + 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); + + // 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("DISABLING 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 (enable) { + powerMon->setState(meshtastic_PowerMon_State_BT_On); + + // If not yet set-up + if (!nrf52Bluetooth) { + LOG_DEBUG("Initializing 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(); + } +} +#else +#warning NRF52 "Bluetooth disable" workaround does not apply to builds with MESHTASTIC_EXCLUDE_BLUETOOTH +void setBluetoothEnable(bool enable) {} +#endif +/** + * 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; +} + +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); + } +} + +void nrf52Loop() +{ + checkSDEvents(); +} + +#ifdef USE_SEMIHOSTING +#include +#include + +/** + * Note: this variable is in BSS and therfore false by default. But the gdbinit + * file will be installing a temporary breakpoint that changes wantSemihost to true. + */ +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); + } +} +#endif + +void nrf52Setup() +{ + 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(); +#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); + +#ifdef BQ25703A_ADDR + 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("Setting random seed %u", seed.seed32); + randomSeed(seed.seed32); + nRFCrypto.end(); +} + +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(); +#endif + SPI.end(); + // This may cause crashes as debug messages continue to flow. + Serial.end(); + +#ifdef PIN_SERIAL_RX1 + Serial1.end(); +#endif + setBluetoothEnable(false); + +#ifdef RAK4630 +#ifdef PIN_3V3_EN + digitalWrite(PIN_3V3_EN, LOW); +#endif +#ifdef AQ_SET_PIN + // 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); +#endif +#endif + +#ifdef HELTEC_MESH_NODE_T114 + nrf_gpio_cfg_default(PIN_GPS_PPS); + detachInterrupt(PIN_GPS_PPS); + detachInterrupt(PIN_BUTTON1); +#endif + // 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_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 + 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("."); + } +} + +void clearBonds() +{ + if (!nrf52Bluetooth) { + nrf52Bluetooth = new NRF52Bluetooth(); + nrf52Bluetooth->setup(); + } + nrf52Bluetooth->clearBonds(); +} + +void enterDfuMode() +{ +// SDK kit does not have native USB like almost all other NRF52 boards +#ifdef NRF_USE_SERIAL_DFU + enterSerialDfu(); +#else + enterUf2Dfu(); +#endif +} \ No newline at end of file diff --git a/src/platform/nrf52/nrf52840_s140_v7.ld b/src/platform/nrf52/nrf52840_s140_v7.ld new file mode 100644 index 0000000..6aaeb40 --- /dev/null +++ b/src/platform/nrf52/nrf52840_s140_v7.ld @@ -0,0 +1,38 @@ +/* Linker script to configure memory regions. */ + +SEARCH_DIR(.) +GROUP(-lgcc -lc -lnosys) + +MEMORY +{ + FLASH (rx) : ORIGIN = 0x27000, LENGTH = 0xED000 - 0x27000 + + /* SRAM required by Softdevice depend on + * - Attribute Table Size (Number of Services and Characteristics) + * - Vendor UUID count + * - Max ATT MTU + * - Concurrent connection peripheral + central + secure links + * - Event Len, HVN queue, Write CMD queue + */ + RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000 +} + +SECTIONS +{ + . = ALIGN(4); + .svc_data : + { + PROVIDE(__start_svc_data = .); + KEEP(*(.svc_data)) + PROVIDE(__stop_svc_data = .); + } > RAM + + .fs_data : + { + PROVIDE(__start_fs_data = .); + KEEP(*(.fs_data)) + PROVIDE(__stop_fs_data = .); + } > RAM +} INSERT AFTER .data; + +INCLUDE "nrf52_common.ld" diff --git a/src/platform/nrf52/softdevice/ble.h b/src/platform/nrf52/softdevice/ble.h new file mode 100644 index 0000000..177b436 --- /dev/null +++ b/src/platform/nrf52/softdevice/ble.h @@ -0,0 +1,652 @@ +/* + * Copyright (c) Nordic Semiconductor ASA + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form, except as embedded into a Nordic + * Semiconductor ASA integrated circuit in a product or a software update for + * such product, must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of Nordic Semiconductor ASA nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * 4. This software, with or without modification, must only be used with a + * Nordic Semiconductor ASA integrated circuit. + * + * 5. Any software provided in binary form under this license must not be reverse + * engineered, decompiled, modified and/or disassembled. + * + * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + @addtogroup BLE_COMMON BLE SoftDevice Common + @{ + @defgroup ble_api Events, type definitions and API calls + @{ + + @brief Module independent events, type definitions and API calls for the BLE SoftDevice. + + */ + +#ifndef BLE_H__ +#define BLE_H__ + +#include "ble_err.h" +#include "ble_gap.h" +#include "ble_gatt.h" +#include "ble_gattc.h" +#include "ble_gatts.h" +#include "ble_l2cap.h" +#include "nrf_error.h" +#include "nrf_svc.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @addtogroup BLE_COMMON_ENUMERATIONS Enumerations + * @{ */ + +/** + * @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. */ +}; + +/** + * @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 */ +}; + +/**@brief BLE Connection Configuration IDs. + * + * 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. */ +}; + +/**@brief BLE Common Configuration IDs. + * + * IDs that uniquely identify a common configuration. + */ +enum BLE_COMMON_CFGS { + 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 */ +}; + +/** @} */ + +/** @addtogroup BLE_COMMON_DEFINES Defines + * @{ */ + +/** @brief Required pointer alignment for BLE Events. + */ +#define BLE_EVT_PTR_ALIGNMENT 4 + +/** @brief Leaves the maximum of the two arguments. + */ +#define BLE_MAX(a, b) ((a) < (b) ? (b) : (a)) + +/** @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. + */ +#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 + * @{ */ +#define BLE_USER_MEM_TYPE_INVALID 0x00 /**< Invalid User Memory Types. */ +#define BLE_USER_MEM_TYPE_GATTS_QUEUED_WRITES 0x01 /**< User Memory for GATTS queued writes. */ +/** @} */ + +/** @defgroup BLE_UUID_VS_COUNTS Vendor Specific base UUID counts + * @{ + */ +#define BLE_UUID_VS_COUNT_DEFAULT 10 /**< Default VS UUID count. */ +#define BLE_UUID_VS_COUNT_MAX 254 /**< Maximum VS UUID count. */ +/** @} */ + +/** @defgroup BLE_COMMON_CFG_DEFAULTS Configuration defaults. + * @{ + */ +#define BLE_CONN_CFG_TAG_DEFAULT 0 /**< Default configuration tag, SoftDevice default connection configuration. */ + +/** @} */ + +/** @} */ + +/** @addtogroup BLE_COMMON_STRUCTURES Structures + * @{ */ + +/**@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. */ +} 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. */ +} 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 */ +} 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. */ +} 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. */ +} 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_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). */ +} 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 */ +} ble_pa_lna_cfg_t; + +/** + * @brief PA & LNA GPIO toggle configuration + * + * This option configures the SoftDevice to toggle pins when the radio is active for use with a power amplifier and/or + * a low noise amplifier. + * + * Toggling the pins is achieved by using two PPI channels and a GPIOTE channel. The hardware channel IDs are provided + * 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. + */ +typedef struct { + 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 */ +} ble_common_opt_pa_lna_t; + +/** + * @brief Configuration of extended BLE connection events. + * + * 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. + * + * @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. */ +} 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 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(). + * + * @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. */ +} 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_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_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, + * 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. + * + * @sa sd_ble_gap_adv_start() + * @sa sd_ble_gap_connect() + * + * @mscs + * @mmsc{@ref BLE_CONN_CFG} + * @endmscs + + */ +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. */ +} ble_conn_cfg_t; + +/** + * @brief Configuration of Vendor Specific base UUIDs, set with @ref sd_ble_cfg_set. + * + * @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. */ +} 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_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_cfg_t; + +/** @} */ + +/** @addtogroup BLE_COMMON_FUNCTIONS Functions + * @{ */ + +/**@brief Enable the BLE stack + * + * @param[in, out] p_app_ram_base Pointer to a variable containing the start address of the + * application RAM region (APP_RAM_BASE). On return, this will + * contain the minimum start address of the application RAM region + * required by the SoftDevice for this configuration. + * @warning After this call, the SoftDevice may generate several events. The list of events provided + * below require the application to initiate a SoftDevice API call. The corresponding API call + * is referenced in the event documentation. + * If the application fails to do so, the BLE connection may timeout, or the SoftDevice may stop + * communicating with the peer device. + * - @ref BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST + * - @ref BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST + * - @ref BLE_GAP_EVT_PHY_UPDATE_REQUEST + * - @ref BLE_GAP_EVT_SEC_PARAMS_REQUEST + * - @ref BLE_GAP_EVT_SEC_INFO_REQUEST + * - @ref BLE_GAP_EVT_SEC_REQUEST + * - @ref BLE_GAP_EVT_AUTH_KEY_REQUEST + * - @ref BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST + * - @ref BLE_EVT_USER_MEM_REQUEST + * - @ref BLE_L2CAP_EVT_CH_SETUP_REQUEST + * + * @note The memory requirement for a specific configuration will not increase between SoftDevices + * with the same major version number. + * + * @note At runtime the IC's RAM is split into 2 regions: The SoftDevice RAM region is located + * between 0x20000000 and APP_RAM_BASE-1 and the application's RAM region is located between + * APP_RAM_BASE and the start of the call stack. + * + * @details This call initializes the BLE stack, no BLE related function other than @ref + * sd_ble_cfg_set can be called before this one. + * + * @mscs + * @mmsc{@ref BLE_COMMON_ENABLE} + * @endmscs + * + * @retval ::NRF_SUCCESS The BLE stack has been initialized successfully. + * @retval ::NRF_ERROR_INVALID_STATE The BLE stack had already been initialized and cannot be reinitialized. + * @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. + * - 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. + */ +SVCALL(SD_BLE_ENABLE, uint32_t, sd_ble_enable(uint32_t *p_app_ram_base)); + +/**@brief Add configurations for the BLE stack + * + * @param[in] cfg_id Config ID, see @ref BLE_CONN_CFGS, @ref BLE_COMMON_CFGS, @ref + * BLE_GAP_CFGS or @ref BLE_GATTS_CFGS. + * @param[in] p_cfg Pointer to a ble_cfg_t structure containing the configuration value. + * @param[in] app_ram_base The start address of the application RAM region (APP_RAM_BASE). + * See @ref sd_ble_enable for details about APP_RAM_BASE. + * + * @note The memory requirement for a specific configuration will not increase between SoftDevices + * with the same major version number. + * + * @note If a configuration is set more than once, the last one set is the one that takes effect on + * @ref sd_ble_enable. + * + * @note Any part of the BLE stack that is NOT configured with @ref sd_ble_cfg_set will have default + * configuration. + * + * @note @ref sd_ble_cfg_set may be called at any time when the SoftDevice is enabled (see @ref + * sd_softdevice_enable) while the BLE part of the SoftDevice is not enabled (see @ref + * sd_ble_enable). + * + * @note Error codes for the configurations are described in the configuration structs. + * + * @mscs + * @mmsc{@ref BLE_COMMON_ENABLE} + * @endmscs + * + * @retval ::NRF_SUCCESS The configuration has been added successfully. + * @retval ::NRF_ERROR_INVALID_STATE The BLE stack had already been initialized. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid or not sufficiently aligned pointer supplied. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid cfg_id supplied. + * @retval ::NRF_ERROR_NO_MEM The amount of memory assigned to the SoftDevice by app_ram_base is not + * large enough to fit this configuration's memory requirement. + */ +SVCALL(SD_BLE_CFG_SET, uint32_t, sd_ble_cfg_set(uint32_t cfg_id, ble_cfg_t const *p_cfg, uint32_t app_ram_base)); + +/**@brief Get an event from the pending events queue. + * + * @param[out] p_dest Pointer to buffer to be filled in with an event, or NULL to retrieve the event length. + * This buffer must be aligned to the extend defined by @ref BLE_EVT_PTR_ALIGNMENT. + * The buffer should be interpreted as a @ref ble_evt_t struct. + * @param[in, out] p_len Pointer the length of the buffer, on return it is filled with the event length. + * + * @details This call allows the application to pull a BLE event from the BLE stack. The application is signaled that + * an event is available from the BLE stack by the triggering of the SD_EVT_IRQn interrupt. + * The application is free to choose whether to call this function from thread mode (main context) or directly from the + * Interrupt Service Routine that maps to SD_EVT_IRQn. In any case however, and because the BLE stack runs at a higher + * priority than the application, this function should be called in a loop (until @ref NRF_ERROR_NOT_FOUND is returned) + * 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, + * @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: + * + * \code + * uint16_t len; + * errcode = sd_ble_evt_get(NULL, &len); + * \endcode + * + * @mscs + * @mmsc{@ref BLE_COMMON_IRQ_EVT_MSC} + * @mmsc{@ref BLE_COMMON_THREAD_EVT_MSC} + * @endmscs + * + * @retval ::NRF_SUCCESS Event pulled and stored into the supplied buffer. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid or not sufficiently aligned pointer supplied. + * @retval ::NRF_ERROR_NOT_FOUND No events ready to be pulled. + * @retval ::NRF_ERROR_DATA_SIZE Event ready but could not fit into the supplied buffer. + */ +SVCALL(SD_BLE_EVT_GET, uint32_t, sd_ble_evt_get(uint8_t *p_dest, uint16_t *p_len)); + +/**@brief Add a Vendor Specific base UUID. + * + * @details This call enables the application to add a Vendor Specific base UUID to the BLE stack's table, for later + * use with all other modules and APIs. This then allows the application to use the shorter, 24-bit @ref ble_uuid_t + * format when dealing with both 16-bit and 128-bit UUIDs without having to check for lengths and having split code + * paths. This is accomplished by extending the grouping mechanism that the Bluetooth SIG standard base UUID uses + * for all other 128-bit UUIDs. The type field in the @ref ble_uuid_t structure is an index (relative to + * @ref BLE_UUID_TYPE_VENDOR_BEGIN) to the table populated by multiple calls to this function, and the UUID field + * in the same structure contains the 2 bytes at indexes 12 and 13. The number of possible 128-bit UUIDs available to + * the application is therefore the number of Vendor Specific UUIDs added with the help of this function times 65536, + * although restricted to modifying bytes 12 and 13 for each of the entries in the supplied array. + * + * @note Bytes 12 and 13 of the provided UUID will not be used internally, since those are always replaced by + * the 16-bit uuid field in @ref ble_uuid_t. + * + * @note If a UUID is already present in the BLE stack's internal table, the corresponding index will be returned in + * p_uuid_type along with an @ref NRF_SUCCESS error code. + * + * @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. + * + * @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. + * @retval ::NRF_ERROR_NO_MEM If there are no more free slots for VS UUIDs. + */ +SVCALL(SD_BLE_UUID_VS_ADD, uint32_t, sd_ble_uuid_vs_add(ble_uuid128_t const *p_vs_uuid, uint8_t *p_uuid_type)); + +/**@brief Remove a Vendor Specific base UUID. + * + * @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. + * + * @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. + * @retval ::NRF_ERROR_INVALID_PARAM If p_uuid_type points to a non-valid UUID type. + * @retval ::NRF_ERROR_FORBIDDEN If the Vendor Specific base UUID is in use by the ATT Server. + */ +SVCALL(SD_BLE_UUID_VS_REMOVE, uint32_t, sd_ble_uuid_vs_remove(uint8_t *p_uuid_type)); + +/** @brief Decode little endian raw UUID bytes (16-bit or 128-bit) into a 24 bit @ref ble_uuid_t structure. + * + * @details The raw UUID bytes excluding bytes 12 and 13 (i.e. bytes 0-11 and 14-15) of p_uuid_le are compared + * to the corresponding ones in each entry of the table of Vendor Specific base UUIDs + * to look for a match. If there is such a match, bytes 12 and 13 are returned as p_uuid->uuid and the index + * relative to @ref BLE_UUID_TYPE_VENDOR_BEGIN as p_uuid->type. + * + * @note If the UUID length supplied is 2, then the type set by this call will always be @ref BLE_UUID_TYPE_BLE. + * + * @param[in] uuid_le_len Length in bytes of the buffer pointed to by p_uuid_le (must be 2 or 16 bytes). + * @param[in] p_uuid_le Pointer pointing to little endian raw UUID bytes. + * @param[out] p_uuid Pointer to a @ref ble_uuid_t structure to be filled in. + * + * @retval ::NRF_SUCCESS Successfully decoded into the @ref ble_uuid_t structure. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_INVALID_LENGTH Invalid UUID length. + * @retval ::NRF_ERROR_NOT_FOUND For a 128-bit UUID, no match in the populated table of UUIDs. + */ +SVCALL(SD_BLE_UUID_DECODE, uint32_t, sd_ble_uuid_decode(uint8_t uuid_le_len, uint8_t const *p_uuid_le, ble_uuid_t *p_uuid)); + +/** @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. + * + * @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). + * @param[out] p_uuid_le Pointer to a buffer where the little endian raw UUID bytes (2 or 16) will be stored. + * + * @retval ::NRF_SUCCESS Successfully encoded into the buffer. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid UUID type. + */ +SVCALL(SD_BLE_UUID_ENCODE, uint32_t, sd_ble_uuid_encode(ble_uuid_t const *p_uuid, uint8_t *p_uuid_le_len, uint8_t *p_uuid_le)); + +/**@brief Get Version Information. + * + * @details This call allows the application to get the BLE stack version information. + * + * @param[out] p_version Pointer to a ble_version_t structure to be filled in. + * + * @retval ::NRF_SUCCESS Version information stored successfully. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_BUSY The BLE stack is busy (typically doing a locally-initiated disconnection procedure). + */ +SVCALL(SD_BLE_VERSION_GET, uint32_t, sd_ble_version_get(ble_version_t *p_version)); + +/**@brief Provide a user memory block. + * + * @note This call can only be used as a response to a @ref BLE_EVT_USER_MEM_REQUEST event issued to the application. + * + * @param[in] conn_handle Connection handle. + * @param[in] p_block Pointer to a user memory block structure or NULL if memory is managed by the application. + * + * @mscs + * @mmsc{@ref BLE_GATTS_QUEUED_WRITE_PEER_CANCEL_MSC} + * @mmsc{@ref BLE_GATTS_QUEUED_WRITE_NOBUF_AUTH_MSC} + * @mmsc{@ref BLE_GATTS_QUEUED_WRITE_NOBUF_NOAUTH_MSC} + * @mmsc{@ref BLE_GATTS_QUEUED_WRITE_BUF_AUTH_MSC} + * @mmsc{@ref BLE_GATTS_QUEUED_WRITE_BUF_NOAUTH_MSC} + * @mmsc{@ref BLE_GATTS_QUEUED_WRITE_QUEUE_FULL_MSC} + * @endmscs + * + * @retval ::NRF_SUCCESS Successfully queued a response to the peer. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_BUSY The stack is busy, process pending events and retry. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. + * @retval ::NRF_ERROR_INVALID_LENGTH Invalid user memory block length supplied. + * @retval ::NRF_ERROR_INVALID_STATE Invalid Connection state or no user memory request pending. + */ +SVCALL(SD_BLE_USER_MEM_REPLY, uint32_t, sd_ble_user_mem_reply(uint16_t conn_handle, ble_user_mem_block_t const *p_block)); + +/**@brief Set a BLE option. + * + * @details This call allows the application to set the value of an option. + * + * @param[in] opt_id Option ID, see @ref BLE_COMMON_OPTS, @ref BLE_GAP_OPTS, and @ref BLE_GATTC_OPTS. + * @param[in] p_opt Pointer to a @ref ble_opt_t structure containing the option value. + * + * @retval ::NRF_SUCCESS Option set successfully. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied, check parameter limits and constraints. + * @retval ::NRF_ERROR_INVALID_STATE Unable to set the parameter at this time. + * @retval ::NRF_ERROR_BUSY The BLE stack is busy or the previous procedure has not completed. + */ +SVCALL(SD_BLE_OPT_SET, uint32_t, sd_ble_opt_set(uint32_t opt_id, ble_opt_t const *p_opt)); + +/**@brief Get a BLE option. + * + * @details This call allows the application to retrieve the value of an option. + * + * @param[in] opt_id Option ID, see @ref BLE_COMMON_OPTS and @ref BLE_GAP_OPTS. + * @param[out] p_opt Pointer to a ble_opt_t structure to be filled in. + * + * @retval ::NRF_SUCCESS Option retrieved successfully. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied, check parameter limits and constraints. + * @retval ::NRF_ERROR_INVALID_STATE Unable to retrieve the parameter at this time. + * @retval ::NRF_ERROR_BUSY The BLE stack is busy or the previous procedure has not completed. + * @retval ::NRF_ERROR_NOT_SUPPORTED This option is not supported. + * + */ +SVCALL(SD_BLE_OPT_GET, uint32_t, sd_ble_opt_get(uint32_t opt_id, ble_opt_t *p_opt)); + +/** @} */ +#ifdef __cplusplus +} +#endif +#endif /* BLE_H__ */ + +/** + @} + @} +*/ diff --git a/src/platform/nrf52/softdevice/ble_err.h b/src/platform/nrf52/softdevice/ble_err.h new file mode 100644 index 0000000..d20f6d1 --- /dev/null +++ b/src/platform/nrf52/softdevice/ble_err.h @@ -0,0 +1,92 @@ +/* + * Copyright (c) Nordic Semiconductor ASA + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form, except as embedded into a Nordic + * Semiconductor ASA integrated circuit in a product or a software update for + * such product, must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of Nordic Semiconductor ASA nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * 4. This software, with or without modification, must only be used with a + * Nordic Semiconductor ASA integrated circuit. + * + * 5. Any software provided in binary form under this license must not be reverse + * engineered, decompiled, modified and/or disassembled. + * + * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + @addtogroup BLE_COMMON + @{ + @addtogroup nrf_error + @{ + @ingroup BLE_COMMON + @} + + @defgroup ble_err General error codes + @{ + + @brief General error code definitions for the BLE API. + + @ingroup BLE_COMMON +*/ +#ifndef NRF_BLE_ERR_H__ +#define NRF_BLE_ERR_H__ + +#include "nrf_error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* @defgroup BLE_ERRORS Error Codes + * @{ */ +#define BLE_ERROR_NOT_ENABLED (NRF_ERROR_STK_BASE_NUM + 0x001) /**< @ref sd_ble_enable has not been called. */ +#define BLE_ERROR_INVALID_CONN_HANDLE (NRF_ERROR_STK_BASE_NUM + 0x002) /**< Invalid connection handle. */ +#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. */ +/** @} */ + +/** @defgroup BLE_ERROR_SUBRANGES Module specific error code subranges + * @brief Assignment of subranges for module specific error codes. + * @note For specific error codes, see ble_.h or ble_error_.h. + * @{ */ +#define NRF_L2CAP_ERR_BASE (NRF_ERROR_STK_BASE_NUM + 0x100) /**< L2CAP specific errors. */ +#define NRF_GAP_ERR_BASE (NRF_ERROR_STK_BASE_NUM + 0x200) /**< GAP specific errors. */ +#define NRF_GATTC_ERR_BASE (NRF_ERROR_STK_BASE_NUM + 0x300) /**< GATT client specific errors. */ +#define NRF_GATTS_ERR_BASE (NRF_ERROR_STK_BASE_NUM + 0x400) /**< GATT server specific errors. */ +/** @} */ + +#ifdef __cplusplus +} +#endif +#endif + +/** + @} + @} +*/ diff --git a/src/platform/nrf52/softdevice/ble_gap.h b/src/platform/nrf52/softdevice/ble_gap.h new file mode 100644 index 0000000..8ebdfa8 --- /dev/null +++ b/src/platform/nrf52/softdevice/ble_gap.h @@ -0,0 +1,2895 @@ +/* + * Copyright (c) Nordic Semiconductor ASA + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form, except as embedded into a Nordic + * Semiconductor ASA integrated circuit in a product or a software update for + * such product, must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of Nordic Semiconductor ASA nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * 4. This software, with or without modification, must only be used with a + * Nordic Semiconductor ASA integrated circuit. + * + * 5. Any software provided in binary form under this license must not be reverse + * engineered, decompiled, modified and/or disassembled. + * + * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + @addtogroup BLE_GAP Generic Access Profile (GAP) + @{ + @brief Definitions and prototypes for the GAP interface. + */ + +#ifndef BLE_GAP_H__ +#define BLE_GAP_H__ + +#include "ble_err.h" +#include "ble_hci.h" +#include "ble_ranges.h" +#include "ble_types.h" +#include "nrf_error.h" +#include "nrf_svc.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/**@addtogroup BLE_GAP_ENUMERATIONS Enumerations + * @{ */ + +/**@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. */ +}; + +/**@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. */ +}; + +/**@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 */ +}; + +/**@brief GAP Configuration IDs. + * + * 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. */ +}; + +/**@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. */ +}; + +/** @} */ + +/**@addtogroup BLE_GAP_DEFINES Defines + * @{ */ + +/**@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. */ +/**@} */ + +/**@defgroup BLE_GAP_ROLES GAP Roles + * @{ */ +#define BLE_GAP_ROLE_INVALID 0x0 /**< Invalid Role. */ +#define BLE_GAP_ROLE_PERIPH 0x1 /**< Peripheral Role. */ +#define BLE_GAP_ROLE_CENTRAL 0x2 /**< Central Role. */ +/**@} */ + +/**@defgroup BLE_GAP_TIMEOUT_SOURCES GAP Timeout sources + * @{ */ +#define BLE_GAP_TIMEOUT_SRC_SCAN 0x01 /**< Scanning timeout. */ +#define BLE_GAP_TIMEOUT_SRC_CONN 0x02 /**< Connection timeout. */ +#define BLE_GAP_TIMEOUT_SRC_AUTH_PAYLOAD 0x03 /**< Authenticated payload timeout. */ +/**@} */ + +/**@defgroup BLE_GAP_ADDR_TYPES GAP Address types + * @{ */ +#define BLE_GAP_ADDR_TYPE_PUBLIC 0x00 /**< Public (identity) address.*/ +#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. */ +/**@} */ + +/**@brief The default interval in seconds at which a private address is refreshed. */ +#define BLE_GAP_DEFAULT_PRIVATE_ADDR_CYCLE_INTERVAL_S (900) /* 15 minutes. */ +/**@brief The maximum interval in seconds at which a private address can be refreshed. */ +#define BLE_GAP_MAX_PRIVATE_ADDR_CYCLE_INTERVAL_S (41400) /* 11 hours 30 minutes. */ + +/** @brief BLE address length. */ +#define BLE_GAP_ADDR_LEN (6) + +/**@defgroup BLE_GAP_PRIVACY_MODES Privacy modes + * @{ */ +#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. */ +/**@} */ + +/** @brief Invalid power level. */ +#define BLE_GAP_POWER_LEVEL_INVALID 127 + +/** @brief Advertising set handle not set. */ +#define BLE_GAP_ADV_SET_HANDLE_NOT_SET (0xFF) + +/** @brief The default number of advertising sets. */ +#define BLE_GAP_ADV_SET_COUNT_DEFAULT (1) + +/** @brief The maximum number of advertising sets supported by this SoftDevice. */ +#define BLE_GAP_ADV_SET_COUNT_MAX (1) + +/**@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_EXTENDED_CONNECTABLE_MAX_SUPPORTED \ + (238) /**< Maximum supported data length for an extended connectable advertising set. */ +/**@}. */ + +/** @brief Set ID not available in advertising report. */ +#define BLE_GAP_ADV_REPORT_SET_ID_NOT_AVAILABLE 0xFF + +/**@defgroup BLE_GAP_EVT_ADV_SET_TERMINATED_REASON GAP Advertising Set Terminated reasons + * @{ */ +#define BLE_GAP_EVT_ADV_SET_TERMINATED_REASON_TIMEOUT 0x01 /**< Timeout value reached. */ +#define BLE_GAP_EVT_ADV_SET_TERMINATED_REASON_LIMIT_REACHED 0x02 /**< @ref ble_gap_adv_params_t::max_adv_evts was reached. */ +/**@} */ + +/**@defgroup BLE_GAP_AD_TYPE_DEFINITIONS GAP Advertising and Scan Response Data format + * @note Found at https://www.bluetooth.org/Technical/AssignedNumbers/generic_access_profile.htm + * @{ */ +#define BLE_GAP_AD_TYPE_FLAGS 0x01 /**< Flags for discoverability. */ +#define BLE_GAP_AD_TYPE_16BIT_SERVICE_UUID_MORE_AVAILABLE 0x02 /**< Partial list of 16 bit service UUIDs. */ +#define BLE_GAP_AD_TYPE_16BIT_SERVICE_UUID_COMPLETE 0x03 /**< Complete list of 16 bit service UUIDs. */ +#define BLE_GAP_AD_TYPE_32BIT_SERVICE_UUID_MORE_AVAILABLE 0x04 /**< Partial list of 32 bit service UUIDs. */ +#define BLE_GAP_AD_TYPE_32BIT_SERVICE_UUID_COMPLETE 0x05 /**< Complete list of 32 bit service UUIDs. */ +#define BLE_GAP_AD_TYPE_128BIT_SERVICE_UUID_MORE_AVAILABLE 0x06 /**< Partial list of 128 bit service UUIDs. */ +#define BLE_GAP_AD_TYPE_128BIT_SERVICE_UUID_COMPLETE 0x07 /**< Complete list of 128 bit service UUIDs. */ +#define BLE_GAP_AD_TYPE_SHORT_LOCAL_NAME 0x08 /**< Short local device name. */ +#define BLE_GAP_AD_TYPE_COMPLETE_LOCAL_NAME 0x09 /**< Complete local device name. */ +#define BLE_GAP_AD_TYPE_TX_POWER_LEVEL 0x0A /**< Transmit power level. */ +#define BLE_GAP_AD_TYPE_CLASS_OF_DEVICE 0x0D /**< Class of device. */ +#define BLE_GAP_AD_TYPE_SIMPLE_PAIRING_HASH_C 0x0E /**< Simple Pairing Hash C. */ +#define BLE_GAP_AD_TYPE_SIMPLE_PAIRING_RANDOMIZER_R 0x0F /**< Simple Pairing Randomizer R. */ +#define BLE_GAP_AD_TYPE_SECURITY_MANAGER_TK_VALUE 0x10 /**< Security Manager TK Value. */ +#define BLE_GAP_AD_TYPE_SECURITY_MANAGER_OOB_FLAGS 0x11 /**< Security Manager Out Of Band Flags. */ +#define BLE_GAP_AD_TYPE_SLAVE_CONNECTION_INTERVAL_RANGE 0x12 /**< Slave Connection Interval Range. */ +#define BLE_GAP_AD_TYPE_SOLICITED_SERVICE_UUIDS_16BIT 0x14 /**< List of 16-bit Service Solicitation UUIDs. */ +#define BLE_GAP_AD_TYPE_SOLICITED_SERVICE_UUIDS_128BIT 0x15 /**< List of 128-bit Service Solicitation UUIDs. */ +#define BLE_GAP_AD_TYPE_SERVICE_DATA 0x16 /**< Service Data - 16-bit UUID. */ +#define BLE_GAP_AD_TYPE_PUBLIC_TARGET_ADDRESS 0x17 /**< Public Target Address. */ +#define BLE_GAP_AD_TYPE_RANDOM_TARGET_ADDRESS 0x18 /**< Random Target Address. */ +#define BLE_GAP_AD_TYPE_APPEARANCE 0x19 /**< Appearance. */ +#define BLE_GAP_AD_TYPE_ADVERTISING_INTERVAL 0x1A /**< Advertising Interval. */ +#define BLE_GAP_AD_TYPE_LE_BLUETOOTH_DEVICE_ADDRESS 0x1B /**< LE Bluetooth Device Address. */ +#define BLE_GAP_AD_TYPE_LE_ROLE 0x1C /**< LE Role. */ +#define BLE_GAP_AD_TYPE_SIMPLE_PAIRING_HASH_C256 0x1D /**< Simple Pairing Hash C-256. */ +#define BLE_GAP_AD_TYPE_SIMPLE_PAIRING_RANDOMIZER_R256 0x1E /**< Simple Pairing Randomizer R-256. */ +#define BLE_GAP_AD_TYPE_SERVICE_DATA_32BIT_UUID 0x20 /**< Service Data - 32-bit UUID. */ +#define BLE_GAP_AD_TYPE_SERVICE_DATA_128BIT_UUID 0x21 /**< Service Data - 128-bit UUID. */ +#define BLE_GAP_AD_TYPE_LESC_CONFIRMATION_VALUE 0x22 /**< LE Secure Connections Confirmation Value */ +#define BLE_GAP_AD_TYPE_LESC_RANDOM_VALUE 0x23 /**< LE Secure Connections Random Value */ +#define BLE_GAP_AD_TYPE_URI 0x24 /**< URI */ +#define BLE_GAP_AD_TYPE_3D_INFORMATION_DATA 0x3D /**< 3D Information Data. */ +#define BLE_GAP_AD_TYPE_MANUFACTURER_SPECIFIC_DATA 0xFF /**< Manufacturer Specific Data. */ +/**@} */ + +/**@defgroup BLE_GAP_ADV_FLAGS GAP Advertisement Flags + * @{ */ +#define BLE_GAP_ADV_FLAG_LE_LIMITED_DISC_MODE (0x01) /**< LE Limited Discoverable Mode. */ +#define BLE_GAP_ADV_FLAG_LE_GENERAL_DISC_MODE (0x02) /**< LE General Discoverable Mode. */ +#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. */ +/**@} */ + +/**@defgroup BLE_GAP_ADV_INTERVALS GAP Advertising interval max and min + * @{ */ +#define BLE_GAP_ADV_INTERVAL_MIN 0x000020 /**< Minimum Advertising interval in 625 us units, i.e. 20 ms. */ +#define BLE_GAP_ADV_INTERVAL_MAX 0x004000 /**< Maximum Advertising interval in 625 us units, i.e. 10.24 s. */ + /**@} */ + +/**@defgroup BLE_GAP_SCAN_INTERVALS GAP Scan interval max and min + * @{ */ +#define BLE_GAP_SCAN_INTERVAL_MIN 0x0004 /**< Minimum Scan interval in 625 us units, i.e. 2.5 ms. */ +#define BLE_GAP_SCAN_INTERVAL_MAX 0xFFFF /**< Maximum Scan interval in 625 us units, i.e. 40,959.375 s. */ + /** @} */ + +/**@defgroup BLE_GAP_SCAN_WINDOW GAP Scan window max and min + * @{ */ +#define BLE_GAP_SCAN_WINDOW_MIN 0x0004 /**< Minimum Scan window in 625 us units, i.e. 2.5 ms. */ +#define BLE_GAP_SCAN_WINDOW_MAX 0xFFFF /**< Maximum Scan window in 625 us units, i.e. 40,959.375 s. */ + /** @} */ + +/**@defgroup BLE_GAP_SCAN_TIMEOUT GAP Scan timeout max and min + * @{ */ +#define BLE_GAP_SCAN_TIMEOUT_MIN 0x0001 /**< Minimum Scan timeout in 10 ms units, i.e 10 ms. */ +#define BLE_GAP_SCAN_TIMEOUT_UNLIMITED 0x0000 /**< Continue to scan forever. */ + /** @} */ + +/**@defgroup BLE_GAP_SCAN_BUFFER_SIZE GAP Minimum scanner buffer size + * + * Scan buffers are used for storing advertising data received from an advertiser. + * 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. */ +/** @} */ + +/**@defgroup BLE_GAP_ADV_TYPES GAP Advertising types + * + * Advertising types defined in Bluetooth Core Specification v5.0, Vol 6, Part B, Section 4.4.2. + * + * The maximum advertising data length is defined by @ref BLE_GAP_ADV_SET_DATA_SIZE_MAX. + * The maximum supported data length for an extended advertiser is defined by + * @ref BLE_GAP_ADV_SET_DATA_SIZE_EXTENDED_MAX_SUPPORTED + * Note that some of the advertising types do not support advertising data. Non-scannable types do not support + * 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. */ +/**@} */ + +/**@defgroup BLE_GAP_ADV_FILTER_POLICIES GAP Advertising filter policies + * @{ */ +#define BLE_GAP_ADV_FP_ANY 0x00 /**< Allow scan requests and connect requests from any device. */ +#define BLE_GAP_ADV_FP_FILTER_SCANREQ 0x01 /**< Filter scan requests with whitelist. */ +#define BLE_GAP_ADV_FP_FILTER_CONNREQ 0x02 /**< Filter connect requests with whitelist. */ +#define BLE_GAP_ADV_FP_FILTER_BOTH 0x03 /**< Filter both scan and connect requests with whitelist. */ +/**@} */ + +/**@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. */ +/**@} */ + +/**@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. */ +/**@} */ + +/**@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. */ +/**@} */ + +/**@defgroup BLE_GAP_DISC_MODES GAP Discovery modes + * @{ */ +#define BLE_GAP_DISC_MODE_NOT_DISCOVERABLE 0x00 /**< Not discoverable discovery Mode. */ +#define BLE_GAP_DISC_MODE_LIMITED 0x01 /**< Limited Discovery Mode. */ +#define BLE_GAP_DISC_MODE_GENERAL 0x02 /**< General Discovery Mode. */ +/**@} */ + +/**@defgroup BLE_GAP_IO_CAPS GAP IO Capabilities + * @{ */ +#define BLE_GAP_IO_CAPS_DISPLAY_ONLY 0x00 /**< Display Only. */ +#define BLE_GAP_IO_CAPS_DISPLAY_YESNO 0x01 /**< Display and Yes/No entry. */ +#define BLE_GAP_IO_CAPS_KEYBOARD_ONLY 0x02 /**< Keyboard Only. */ +#define BLE_GAP_IO_CAPS_NONE 0x03 /**< No I/O capabilities. */ +#define BLE_GAP_IO_CAPS_KEYBOARD_DISPLAY 0x04 /**< Keyboard and Display. */ +/**@} */ + +/**@defgroup BLE_GAP_AUTH_KEY_TYPES GAP Authentication Key Types + * @{ */ +#define BLE_GAP_AUTH_KEY_TYPE_NONE 0x00 /**< No key (may be used to reject). */ +#define BLE_GAP_AUTH_KEY_TYPE_PASSKEY 0x01 /**< 6-digit Passkey. */ +#define BLE_GAP_AUTH_KEY_TYPE_OOB 0x02 /**< Out Of Band data. */ +/**@} */ + +/**@defgroup BLE_GAP_KP_NOT_TYPES GAP Keypress Notification Types + * @{ */ +#define BLE_GAP_KP_NOT_TYPE_PASSKEY_START 0x00 /**< Passkey entry started. */ +#define BLE_GAP_KP_NOT_TYPE_PASSKEY_DIGIT_IN 0x01 /**< Passkey digit entered. */ +#define BLE_GAP_KP_NOT_TYPE_PASSKEY_DIGIT_OUT 0x02 /**< Passkey digit erased. */ +#define BLE_GAP_KP_NOT_TYPE_PASSKEY_CLEAR 0x03 /**< Passkey cleared. */ +#define BLE_GAP_KP_NOT_TYPE_PASSKEY_END 0x04 /**< Passkey entry completed. */ +/**@} */ + +/**@defgroup BLE_GAP_SEC_STATUS GAP Security status + * @{ */ +#define BLE_GAP_SEC_STATUS_SUCCESS 0x00 /**< Procedure completed with success. */ +#define BLE_GAP_SEC_STATUS_TIMEOUT 0x01 /**< Procedure timed out. */ +#define BLE_GAP_SEC_STATUS_PDU_INVALID 0x02 /**< Invalid PDU received. */ +#define BLE_GAP_SEC_STATUS_RFU_RANGE1_BEGIN 0x03 /**< Reserved for Future Use range #1 begin. */ +#define BLE_GAP_SEC_STATUS_RFU_RANGE1_END 0x80 /**< Reserved for Future Use range #1 end. */ +#define BLE_GAP_SEC_STATUS_PASSKEY_ENTRY_FAILED 0x81 /**< Passkey entry failed (user canceled or other). */ +#define BLE_GAP_SEC_STATUS_OOB_NOT_AVAILABLE 0x82 /**< Out of Band Key not available. */ +#define BLE_GAP_SEC_STATUS_AUTH_REQ 0x83 /**< Authentication requirements not met. */ +#define BLE_GAP_SEC_STATUS_CONFIRM_VALUE 0x84 /**< Confirm value failed. */ +#define BLE_GAP_SEC_STATUS_PAIRING_NOT_SUPP 0x85 /**< Pairing not supported. */ +#define BLE_GAP_SEC_STATUS_ENC_KEY_SIZE 0x86 /**< Encryption key size. */ +#define BLE_GAP_SEC_STATUS_SMP_CMD_UNSUPPORTED 0x87 /**< Unsupported SMP command. */ +#define BLE_GAP_SEC_STATUS_UNSPECIFIED 0x88 /**< Unspecified reason. */ +#define BLE_GAP_SEC_STATUS_REPEATED_ATTEMPTS 0x89 /**< Too little time elapsed since last attempt. */ +#define BLE_GAP_SEC_STATUS_INVALID_PARAMS 0x8A /**< Invalid parameters. */ +#define BLE_GAP_SEC_STATUS_DHKEY_FAILURE 0x8B /**< DHKey check failure. */ +#define BLE_GAP_SEC_STATUS_NUM_COMP_FAILURE 0x8C /**< Numeric Comparison failure. */ +#define BLE_GAP_SEC_STATUS_BR_EDR_IN_PROG 0x8D /**< BR/EDR pairing in progress. */ +#define BLE_GAP_SEC_STATUS_X_TRANS_KEY_DISALLOWED 0x8E /**< BR/EDR Link Key cannot be used for LE keys. */ +#define BLE_GAP_SEC_STATUS_RFU_RANGE2_BEGIN 0x8F /**< Reserved for Future Use range #2 begin. */ +#define BLE_GAP_SEC_STATUS_RFU_RANGE2_END 0xFF /**< Reserved for Future Use range #2 end. */ +/**@} */ + +/**@defgroup BLE_GAP_SEC_STATUS_SOURCES GAP Security status sources + * @{ */ +#define BLE_GAP_SEC_STATUS_SOURCE_LOCAL 0x00 /**< Local failure. */ +#define BLE_GAP_SEC_STATUS_SOURCE_REMOTE 0x01 /**< Remote failure. */ +/**@} */ + +/**@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_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_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. */ +/**@} */ + +/**@defgroup BLE_GAP_DEVNAME GAP device name defines. + * @{ */ +#define BLE_GAP_DEVNAME_DEFAULT "nRF5x" /**< Default device name value. */ +#define BLE_GAP_DEVNAME_DEFAULT_LEN 31 /**< Default number of octets in device name. */ +#define BLE_GAP_DEVNAME_MAX_LEN 248 /**< Maximum number of octets in device name. */ +/**@} */ + +/**@brief Disable RSSI events for connections */ +#define BLE_GAP_RSSI_THRESHOLD_INVALID 0xFF + +/**@defgroup BLE_GAP_PHYS GAP PHYs + * @{ */ +#define BLE_GAP_PHY_AUTO 0x00 /**< Automatic PHY selection. Refer @ref sd_ble_gap_phy_update for more information.*/ +#define BLE_GAP_PHY_1MBPS 0x01 /**< 1 Mbps PHY. */ +#define BLE_GAP_PHY_2MBPS 0x02 /**< 2 Mbps PHY. */ +#define BLE_GAP_PHY_CODED 0x04 /**< Coded PHY. */ +#define BLE_GAP_PHY_NOT_SET 0xFF /**< PHY is not configured. */ + +/**@brief Supported PHYs in connections, for scanning, and for advertising. */ +#define BLE_GAP_PHYS_SUPPORTED (BLE_GAP_PHY_1MBPS | BLE_GAP_PHY_2MBPS | BLE_GAP_PHY_CODED) /**< All PHYs are supported. */ + +/**@} */ + +/**@defgroup BLE_GAP_CONN_SEC_MODE_SET_MACROS GAP attribute security requirement setters + * + * 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) +/**@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) +/**@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) +/**@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) +/**@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) +/**@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) +/**@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) +/**@} */ + +/**@brief GAP Security Random Number Length. */ +#define BLE_GAP_SEC_RAND_LEN 8 + +/**@brief GAP Security Key Length. */ +#define BLE_GAP_SEC_KEY_LEN 16 + +/**@brief GAP LE Secure Connections Elliptic Curve Diffie-Hellman P-256 Public Key Length. */ +#define BLE_GAP_LESC_P256_PK_LEN 64 + +/**@brief GAP LE Secure Connections Elliptic Curve Diffie-Hellman DHKey Length. */ +#define BLE_GAP_LESC_DHKEY_LEN 32 + +/**@brief GAP Passkey Length. */ +#define BLE_GAP_PASSKEY_LEN 6 + +/**@brief Maximum amount of addresses in the whitelist. */ +#define BLE_GAP_WHITELIST_ADDR_MAX_COUNT (8) + +/**@brief Maximum amount of identities in the device identities list. */ +#define BLE_GAP_DEVICE_IDENTITIES_MAX_COUNT (8) + +/**@brief Default connection count for a configuration. */ +#define BLE_GAP_CONN_COUNT_DEFAULT (1) + +/**@defgroup BLE_GAP_EVENT_LENGTH GAP event length defines. + * @{ */ +#define BLE_GAP_EVENT_LENGTH_MIN (2) /**< Minimum event length, in 1.25 ms units. */ +#define BLE_GAP_EVENT_LENGTH_CODED_PHY_MIN (6) /**< The shortest event length in 1.25 ms units supporting LE Coded PHY. */ +#define BLE_GAP_EVENT_LENGTH_DEFAULT (3) /**< Default event length, in 1.25 ms units. */ +/**@} */ + +/**@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. */ + +/**@} */ + +/**@brief Automatic data length parameter. */ +#define BLE_GAP_DATA_LENGTH_AUTO 0 + +/**@defgroup BLE_GAP_AUTH_PAYLOAD_TIMEOUT Authenticated payload timeout defines. + * @{ */ +#define BLE_GAP_AUTH_PAYLOAD_TIMEOUT_MAX (48000) /**< Maximum authenticated payload timeout in 10 ms units, i.e. 8 minutes. */ +#define BLE_GAP_AUTH_PAYLOAD_TIMEOUT_MIN (1) /**< Minimum authenticated payload timeout in 10 ms units, i.e. 10 ms. */ +/**@} */ + +/**@defgroup GAP_SEC_MODES GAP Security Modes + * @{ */ +#define BLE_GAP_SEC_MODE 0x00 /**< No key (may be used to reject). */ +/**@} */ + +/**@brief The total number of channels in Bluetooth Low Energy. */ +#define BLE_GAP_CHANNEL_COUNT (40) + +/**@defgroup BLE_GAP_QOS_CHANNEL_SURVEY_INTERVALS Quality of Service (QoS) Channel survey interval defines + * @{ */ +#define BLE_GAP_QOS_CHANNEL_SURVEY_INTERVAL_CONTINUOUS (0) /**< Continuous channel survey. */ +#define BLE_GAP_QOS_CHANNEL_SURVEY_INTERVAL_MIN_US (7500) /**< Minimum channel survey interval in microseconds (7.5 ms). */ +#define BLE_GAP_QOS_CHANNEL_SURVEY_INTERVAL_MAX_US (4000000) /**< Maximum channel survey interval in microseconds (4 s). */ + /**@} */ + +/** @} */ + +/** @defgroup BLE_GAP_CHAR_INCL_CONFIG GAP Characteristic inclusion configurations + * @{ + */ +#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. */ +/**@} */ + +/** @defgroup BLE_GAP_CHAR_INCL_CONFIG_DEFAULTS Characteristic inclusion default values + * @{ */ +#define BLE_GAP_PPCP_INCL_CONFIG_DEFAULT (BLE_GAP_CHAR_INCL_CONFIG_INCLUDE) /**< Included by default. */ +#define BLE_GAP_CAR_INCL_CONFIG_DEFAULT (BLE_GAP_CHAR_INCL_CONFIG_INCLUDE) /**< Included by default. */ +/**@} */ + +/** @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. */ +/**@} */ + +/**@addtogroup BLE_GAP_STRUCTURES Structures + * @{ */ + +/**@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. */ +} 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. */ +} 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. */ +} 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. */ +} ble_gap_addr_t; + +/**@brief GAP connection parameters. + * + * @note When ble_conn_params_t is received in an event, both min_conn_interval and + * max_conn_interval will be equal to the connection interval set by the central. + * + * @note If both conn_sup_timeout and max_conn_interval are specified, then the following constraint applies: + * conn_sup_timeout * 4 > (1 + slave_latency) * max_conn_interval + * that corresponds to the following Bluetooth Spec requirement: + * The Supervision_Timeout in milliseconds shall be larger than + * (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.*/ +} 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 + */ +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. */ + +} 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_t; + +/**@brief Identity Resolving Key. */ +typedef struct { + uint8_t irk[BLE_GAP_SEC_KEY_LEN]; /**< Array containing IRK. */ +} ble_gap_irk_t; + +/**@brief Channel mask (40 bits). + * Every channel is represented with a bit positioned as per channel index defined in Bluetooth Core Specification v5.0, + * Vol 6, Part B, Section 1.4.1. The LSB contained in array element 0 represents channel index 0, and bit 39 represents + * channel index 39. If a bit is set to 1, the channel is not used. + */ +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_params_t; + +/**@brief GAP advertising data buffers. + * + * The application must provide the buffers for advertisement. The memory shall reside in application RAM, and + * shall never be modified while advertising. The data shall be kept alive until either: + * - @ref BLE_GAP_EVT_ADV_SET_TERMINATED is raised. + * - @ref BLE_GAP_EVT_CONNECTED is raised with @ref ble_gap_evt_connected_t::adv_handle set to the corresponding + * advertising handle. + * - Advertising is stopped. + * - 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_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. */ +} ble_gap_scan_params_t; + +/**@brief Privacy. + * + * The privacy feature provides a way for the device to avoid being tracked over a period of time. + * The privacy feature, when enabled, hides the local device identity and replaces it with a private address + * 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. + * + * 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. + */ +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. */ +} 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. + * @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; + * @endcode + * + */ +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. */ +} 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. */ +} 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. */ +} 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. */ +} 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. */ +} ble_gap_master_id_t; + +/**@brief GAP Signing Information. */ +typedef struct { + 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. */ +} 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. */ +} 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_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_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. */ +} 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_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_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. */ +} 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_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_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. */ +} 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. */ +} 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. */ +} 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_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. */ +} 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_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_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_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_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. */ +} 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. */ +} 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. */ +} 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_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. */ +} 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). */ +} 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. */ +} ble_gap_evt_adv_set_terminated_t; + +/**@brief Event structure for @ref BLE_GAP_EVT_ADV_REPORT. + * + * @note If @ref ble_gap_adv_report_type_t::status is set to @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA, + * not all fields in the advertising report may be available. + * + * @note When ble_gap_adv_report_type_t::status is not set to @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA, + * 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_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. */ +} 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_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. */ +} 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_evt_data_length_update_request_t; + +/**@brief Event structure for @ref BLE_GAP_EVT_DATA_LENGTH_UPDATE. + * + * @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_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. */ +} 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. */ +} ble_gap_evt_t; + +/** + * @brief BLE GAP connection configuration parameters, set with @ref sd_ble_cfg_set. + * + * @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 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. */ +} ble_gap_conn_cfg_t; + +/** + * @brief Configuration of maximum concurrent connections in the different connected roles, set with + * @ref sd_ble_cfg_set. + * + * @retval ::NRF_ERROR_CONN_COUNT The sum of periph_role_count and central_role_count is too + * large. The maximum supported sum of concurrent connections is + * @ref BLE_GAP_ROLE_COUNT_COMBINED_MAX. + * @retval ::NRF_ERROR_INVALID_PARAM central_sec_count is larger than central_role_count. + * @retval ::NRF_ERROR_RESOURCES The adv_set_count is too large. The maximum + * supported advertising handles is + * @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. */ +} ble_gap_cfg_role_count_t; + +/** + * @brief Device name and its properties, set with @ref sd_ble_cfg_set. + * + * @note If the device name is not configured, the default device name will be + * @ref BLE_GAP_DEVNAME_DEFAULT, the maximum device name length will be + * @ref BLE_GAP_DEVNAME_DEFAULT_LEN, vloc will be set to @ref BLE_GATTS_VLOC_STACK and the device name + * will have no write access. + * + * @note If @ref max_len is more than @ref BLE_GAP_DEVNAME_DEFAULT_LEN and vloc is set to @ref BLE_GATTS_VLOC_STACK, + * the attribute table size must be increased to have room for the longer device name (see + * @ref sd_ble_cfg_set and @ref ble_gatts_cfg_attr_tab_size_t). + * + * @note If vloc is @ref BLE_GATTS_VLOC_STACK : + * - p_value must point to non-volatile memory (flash) or be NULL. + * - If p_value is NULL, the device name will initially be empty. + * + * @note If vloc is @ref BLE_GATTS_VLOC_USER : + * - p_value cannot be NULL. + * - If the device name is writable, p_value must point to volatile memory (RAM). + * + * @retval ::NRF_ERROR_INVALID_PARAM One or more of the following is true: + * - 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 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_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. */ +} 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. */ +} 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_t; + +/**@brief Channel Map option. + * + * @details Used with @ref sd_ble_opt_get to get the current channel map + * or @ref sd_ble_opt_set to set a new channel map. When setting the + * channel map, it applies to all current and future connections. When getting the + * current channel map, it applies to a single connection and the connection handle + * must be supplied. + * + * @note Setting the channel map may take some time, depending on connection parameters. + * The time taken may be different for each connection and the get operation will + * return the previous channel map until the new one has taken effect. + * + * @note After setting the channel map, by spec it can not be set again until at least 1 s has passed. + * See Bluetooth Specification Version 4.1 Volume 2, Part E, Section 7.3.46. + * + * @retval ::NRF_SUCCESS Get or set successful. + * @retval ::NRF_ERROR_INVALID_PARAM One or more of the following is true: + * - Less then two bits in @ref ch_map are set. + * - Bits for primary advertising channels (37-39) are set. + * @retval ::NRF_ERROR_BUSY Channel map was set again before enough time had passed. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied for get. + * + */ +typedef struct { + 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. + * + * @details Local connection latency is a feature which enables the slave to improve + * current consumption by ignoring the slave latency set by the peer. The + * local connection latency can only be set to a multiple of the slave latency, + * and cannot be longer than half of the supervision timeout. + * + * @details Used with @ref sd_ble_opt_set to set the local connection latency. The + * @ref sd_ble_opt_get is not supported for this option, but the actual + * local connection latency (unless set to NULL) is set as a return parameter + * when setting the option. + * + * @note The latency set will be truncated down to the closest slave latency event + * multiple, or the nearest multiple before half of the supervision timeout. + * + * @note The local connection latency is disabled by default, and needs to be enabled for new + * connections and whenever the connection is updated. + * + * @retval ::NRF_SUCCESS Set successfully. + * @retval ::NRF_ERROR_NOT_SUPPORTED Get is not supported. + * @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). */ +} ble_gap_opt_local_conn_latency_t; + +/**@brief Disable slave latency + * + * @details Used with @ref sd_ble_opt_set to temporarily disable slave latency of a peripheral connection + * (see @ref ble_gap_conn_params_t::slave_latency). And to re-enable it again. When disabled, the + * peripheral will ignore the slave_latency set by the central. + * + * @note Shall only be called on peripheral links. + * + * @retval ::NRF_SUCCESS Set successfully. + * @retval ::NRF_ERROR_NOT_SUPPORTED Get is not supported. + * @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 */ +} ble_gap_opt_slave_latency_disable_t; + +/**@brief Passkey Option. + * + * @mscs + * @mmsc{@ref BLE_GAP_PERIPH_BONDING_STATIC_PK_MSC} + * @endmscs + * + * @details Structure containing the passkey to be used during pairing. This can be used with @ref + * sd_ble_opt_set to make the SoftDevice use a preprogrammed passkey for authentication + * instead of generating a random one. + * + * @note Repeated pairing attempts using the same preprogrammed passkey makes pairing vulnerable to MITM attacks. + * + * @note @ref sd_ble_opt_get is not supported for this option. + * + */ +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.*/ +} ble_gap_opt_passkey_t; + +/**@brief Compatibility mode 1 option. + * + * @details This can be used with @ref sd_ble_opt_set to enable and disable + * compatibility mode 1. Compatibility mode 1 is disabled by default. + * + * @note Compatibility mode 1 enables interoperability with devices that do not support a value of + * 0 for the WinOffset parameter in the Link Layer CONNECT_IND packet. This applies to a + * limited set of legacy peripheral devices from another vendor. Enabling this compatibility + * mode will only have an effect if the local device will act as a central device and + * initiate a connection to a peripheral device. In that case it may lead to the connection + * creation taking up to one connection interval longer to complete for all connections. + * + * @retval ::NRF_SUCCESS Set successfully. + * @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.*/ +} ble_gap_opt_compat_mode_1_t; + +/**@brief Authenticated payload timeout option. + * + * @details This can be used with @ref sd_ble_opt_set to change the Authenticated payload timeout to a value other + * than the default of @ref BLE_GAP_AUTH_PAYLOAD_TIMEOUT_MAX. + * + * @note The authenticated payload timeout event ::BLE_GAP_TIMEOUT_SRC_AUTH_PAYLOAD will be generated + * if auth_payload_timeout time has elapsed without receiving a packet with a valid MIC on an encrypted + * link. + * + * @note The LE ping procedure will be initiated before the timer expires to give the peer a chance + * to reset the timer. In addition the stack will try to prioritize running of LE ping over other + * activities to increase chances of finishing LE ping before timer expires. To avoid side-effects + * on other activities, it is recommended to use high timeout values. + * Recommended timeout > 2*(connInterval * (6 + connSlaveLatency)). + * + * @retval ::NRF_SUCCESS Set successfully. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. auth_payload_timeout was outside of allowed range. + * @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. */ +} 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_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. */ +} ble_gap_conn_event_trigger_t; +/**@} */ + +/**@addtogroup BLE_GAP_FUNCTIONS Functions + * @{ */ + +/**@brief Set the local Bluetooth identity address. + * + * The local Bluetooth identity address is the address that identifies this device to other peers. + * The address type must be either @ref BLE_GAP_ADDR_TYPE_PUBLIC or @ref BLE_GAP_ADDR_TYPE_RANDOM_STATIC. + * + * @note The identity address cannot be changed while advertising, scanning or creating a connection. + * + * @note This address will be distributed to the peer during bonding. + * If the address changes, the address stored in the peer device will not be valid and the ability to + * reconnect using the old address will be lost. + * + * @note By default the SoftDevice will set an address of type @ref BLE_GAP_ADDR_TYPE_RANDOM_STATIC upon being + * enabled. The address is a random number populated during the IC manufacturing process and remains unchanged + * for the lifetime of each IC. + * + * @mscs + * @mmsc{@ref BLE_GAP_ADV_MSC} + * @endmscs + * + * @param[in] p_addr Pointer to address structure. + * + * @retval ::NRF_SUCCESS Address successfully set. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::BLE_ERROR_GAP_INVALID_BLE_ADDR Invalid address. + * @retval ::NRF_ERROR_BUSY The stack is busy, process pending events and retry. + * @retval ::NRF_ERROR_INVALID_STATE The identity address cannot be changed while advertising, + * scanning or creating a connection. + */ +SVCALL(SD_BLE_GAP_ADDR_SET, uint32_t, sd_ble_gap_addr_set(ble_gap_addr_t const *p_addr)); + +/**@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. + * + * @param[out] p_addr Pointer to address structure to be filled in. + * + * @retval ::NRF_SUCCESS Address successfully retrieved. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid or NULL pointer supplied. + */ +SVCALL(SD_BLE_GAP_ADDR_GET, uint32_t, sd_ble_gap_addr_get(ble_gap_addr_t *p_addr)); + +/**@brief Get the Bluetooth device address used by the advertiser. + * + * @note This function will return the local Bluetooth address used in advertising PDUs. When + * using privacy, the SoftDevice will generate a new private address every + * @ref ble_gap_privacy_params_t::private_addr_cycle_s configured using + * @ref sd_ble_gap_privacy_set. Hence depending on when the application calls this API, the + * address returned may not be the latest address that is used in the advertising PDUs. + * + * @param[in] adv_handle The advertising handle to get the address from. + * @param[out] p_addr Pointer to address structure to be filled in. + * + * @retval ::NRF_SUCCESS Address successfully retrieved. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid or NULL pointer supplied. + * @retval ::BLE_ERROR_INVALID_ADV_HANDLE The provided advertising handle was not found. + * @retval ::NRF_ERROR_INVALID_STATE The advertising set is currently not advertising. + */ +SVCALL(SD_BLE_GAP_ADV_ADDR_GET, uint32_t, sd_ble_gap_adv_addr_get(uint8_t adv_handle, ble_gap_addr_t *p_addr)); + +/**@brief Set the active whitelist in the SoftDevice. + * + * @note Only one whitelist can be used at a time and the whitelist is shared between the BLE roles. + * The whitelist cannot be set if a BLE role is using the whitelist. + * + * @note If an address is resolved using the information in the device identity list, then the whitelist + * filter policy applies to the peer identity address and not the resolvable address sent on air. + * + * @mscs + * @mmsc{@ref BLE_GAP_WL_SHARE_MSC} + * @mmsc{@ref BLE_GAP_PRIVACY_SCAN_PRIVATE_SCAN_MSC} + * @endmscs + * + * @param[in] pp_wl_addrs Pointer to a whitelist of peer addresses, if NULL the whitelist will be cleared. + * @param[in] len Length of the whitelist, maximum @ref BLE_GAP_WHITELIST_ADDR_MAX_COUNT. + * + * @retval ::NRF_SUCCESS The whitelist is successfully set/cleared. + * @retval ::NRF_ERROR_INVALID_ADDR The whitelist (or one of its entries) provided is invalid. + * @retval ::BLE_ERROR_GAP_WHITELIST_IN_USE The whitelist is in use by a BLE role and cannot be set or cleared. + * @retval ::BLE_ERROR_GAP_INVALID_BLE_ADDR Invalid address type is supplied. + * @retval ::NRF_ERROR_DATA_SIZE The given whitelist size is invalid (zero or too large); this can only return when + * pp_wl_addrs is not NULL. + */ +SVCALL(SD_BLE_GAP_WHITELIST_SET, uint32_t, sd_ble_gap_whitelist_set(ble_gap_addr_t const *const *pp_wl_addrs, uint8_t len)); + +/**@brief Set device identity list. + * + * @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] len Length of the device identity list, maximum @ref BLE_GAP_DEVICE_IDENTITIES_MAX_COUNT. + * + * @mscs + * @mmsc{@ref BLE_GAP_PRIVACY_ADV_MSC} + * @mmsc{@ref BLE_GAP_PRIVACY_SCAN_MSC} + * @mmsc{@ref BLE_GAP_PRIVACY_SCAN_PRIVATE_SCAN_MSC} + * @mmsc{@ref BLE_GAP_PRIVACY_ADV_DIR_PRIV_MSC} + * @mmsc{@ref BLE_GAP_PERIPH_CONN_PRIV_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_CONN_PRIV_MSC} + * @endmscs + * + * @retval ::NRF_SUCCESS The device identity list successfully set/cleared. + * @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_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)); + +/**@brief Set privacy settings. + * + * @note Privacy settings cannot be changed while advertising, scanning or creating a connection. + * + * @param[in] p_privacy_params Privacy settings. + * + * @mscs + * @mmsc{@ref BLE_GAP_PRIVACY_ADV_MSC} + * @mmsc{@ref BLE_GAP_PRIVACY_SCAN_MSC} + * @mmsc{@ref BLE_GAP_PRIVACY_ADV_DIR_PRIV_MSC} + * @endmscs + * + * @retval ::NRF_SUCCESS Set successfully. + * @retval ::NRF_ERROR_BUSY The stack is busy, process pending events and retry. + * @retval ::BLE_ERROR_GAP_INVALID_BLE_ADDR Invalid address type is supplied. + * @retval ::NRF_ERROR_INVALID_ADDR The pointer to privacy settings is NULL or invalid. + * Otherwise, the p_device_irk pointer in privacy parameter is an invalid pointer. + * @retval ::NRF_ERROR_INVALID_PARAM Out of range parameters are provided. + * @retval ::NRF_ERROR_NOT_SUPPORTED The SoftDevice does not support privacy if the Central Address Resolution + characteristic is not configured to be included and the SoftDevice is configured + to support central roles. + See @ref ble_gap_cfg_car_incl_cfg_t and @ref ble_gap_cfg_role_count_t. + * @retval ::NRF_ERROR_INVALID_STATE Privacy settings cannot be changed while advertising, scanning + * or creating a connection. + */ +SVCALL(SD_BLE_GAP_PRIVACY_SET, uint32_t, sd_ble_gap_privacy_set(ble_gap_privacy_params_t const *p_privacy_params)); + +/**@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. + * + * @param[in,out] p_privacy_params Privacy settings. + * + * @retval ::NRF_SUCCESS Privacy settings read. + * @retval ::NRF_ERROR_INVALID_ADDR The pointer given for returning the privacy settings may be NULL or invalid. + * Otherwise, the p_device_irk pointer in privacy parameter is an invalid pointer. + */ +SVCALL(SD_BLE_GAP_PRIVACY_GET, uint32_t, sd_ble_gap_privacy_get(ble_gap_privacy_params_t *p_privacy_params)); + +/**@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. + * + * @note In order to update advertising data while advertising, new advertising buffers must be provided. + * + * @mscs + * @mmsc{@ref BLE_GAP_ADV_MSC} + * @mmsc{@ref BLE_GAP_WL_SHARE_MSC} + * @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 + * @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. + * + * @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. + * - 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. + * @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. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_INVALID_FLAGS Invalid combination of advertising flags supplied. + * @retval ::NRF_ERROR_INVALID_DATA Invalid data type(s) supplied. Check the advertising data format + * 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 ::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)); + +/**@brief Start advertising (GAP Discoverable, Connectable modes, Broadcast Procedure). + * + * @note Only one advertiser may be active at any time. + * + * @note If privacy is enabled, the advertiser's private address will be refreshed when this function is called. + * See @ref sd_ble_gap_privacy_set(). + * + * @events + * @event{@ref BLE_GAP_EVT_CONNECTED, Generated after connection has been established through connectable advertising.} + * @event{@ref BLE_GAP_EVT_ADV_SET_TERMINATED, Advertising set has terminated.} + * @event{@ref BLE_GAP_EVT_SCAN_REQ_REPORT, A scan request was received.} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_GAP_ADV_MSC} + * @mmsc{@ref BLE_GAP_PERIPH_CONN_PRIV_MSC} + * @mmsc{@ref BLE_GAP_PRIVACY_ADV_DIR_PRIV_MSC} + * @mmsc{@ref BLE_GAP_WL_SHARE_MSC} + * @endmscs + * + * @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 + * advertising, this is ignored. + * + * @retval ::NRF_SUCCESS The BLE stack has started advertising. + * @retval ::NRF_ERROR_INVALID_STATE adv_handle is not configured or already advertising. + * @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. + * @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. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied: + * - 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 ::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 + 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)); + +/**@brief Stop advertising (GAP Discoverable, Connectable modes, Broadcast Procedure). + * + * @mscs + * @mmsc{@ref BLE_GAP_ADV_MSC} + * @mmsc{@ref BLE_GAP_WL_SHARE_MSC} + * @endmscs + * + * @param[in] adv_handle The advertising handle that should stop advertising. + * + * @retval ::NRF_SUCCESS The BLE stack has stopped advertising. + * @retval ::BLE_ERROR_INVALID_ADV_HANDLE Invalid advertising handle. + * @retval ::NRF_ERROR_INVALID_STATE The advertising handle is not advertising. + */ +SVCALL(SD_BLE_GAP_ADV_STOP, uint32_t, sd_ble_gap_adv_stop(uint8_t adv_handle)); + +/**@brief Update connection parameters. + * + * @details In the central role this will initiate a Link Layer connection parameter update procedure, + * otherwise in the peripheral role, this will send the corresponding L2CAP request and wait for + * 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. + * + * @events + * @event{@ref BLE_GAP_EVT_CONN_PARAM_UPDATE, Result of the connection parameter update procedure.} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_GAP_CPU_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_ENC_AUTH_MUTEX_MSC} + * @mmsc{@ref BLE_GAP_MULTILINK_CPU_MSC} + * @mmsc{@ref BLE_GAP_MULTILINK_CTRL_PROC_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_CPU_MSC} + * @endmscs + * + * @param[in] conn_handle Connection handle. + * @param[in] p_conn_params Pointer to desired connection parameters. If NULL is provided on a peripheral role, + * the parameters in the PPCP characteristic of the GAP service will be used instead. + * If NULL is provided on a central role and in response to a @ref + * BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST, the peripheral request will be rejected + * + * @retval ::NRF_SUCCESS The Connection Update procedure has been started successfully. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied, check parameter limits and constraints. + * @retval ::NRF_ERROR_INVALID_STATE Disconnection in progress or link has not been established. + * @retval ::NRF_ERROR_BUSY Procedure already in progress, wait for pending procedures to complete and retry. + * @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)); + +/**@brief Disconnect (GAP Link Termination). + * + * @details This call initiates the disconnection procedure, and its completion will be communicated to the application + * with a @ref BLE_GAP_EVT_DISCONNECTED event. + * + * @events + * @event{@ref BLE_GAP_EVT_DISCONNECTED, Generated when disconnection procedure is complete.} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_GAP_CONN_MSC} + * @endmscs + * + * @param[in] conn_handle Connection handle. + * @param[in] hci_status_code HCI status code, see @ref BLE_HCI_STATUS_CODES (accepted values are @ref + * BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION and @ref BLE_HCI_CONN_INTERVAL_UNACCEPTABLE). + * + * @retval ::NRF_SUCCESS The disconnection procedure has been started successfully. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. + * @retval ::NRF_ERROR_INVALID_STATE Disconnection in progress or link has not been established. + */ +SVCALL(SD_BLE_GAP_DISCONNECT, uint32_t, sd_ble_gap_disconnect(uint16_t conn_handle, uint8_t hci_status_code)); + +/**@brief Set the radio's transmit power. + * + * @param[in] role The role to set the transmit power for, see @ref BLE_GAP_TX_POWER_ROLES for + * 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 + * @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). + * + * @note Supported tx_power values: -40dBm, -20dBm, -16dBm, -12dBm, -8dBm, -4dBm, 0dBm, +3dBm and +4dBm. + * In addition, on some chips following values are supported: +2dBm, +5dBm, +6dBm, +7dBm and +8dBm. + * Setting these values on a chip that does not support them will result in undefined behaviour. + * @note The initiator will have the same transmit power as the scanner. + * @note When a connection is created it will inherit the transmit power from the initiator or + * advertiser leading to the connection. + * + * @retval ::NRF_SUCCESS Successfully changed the transmit power. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. + * @retval ::BLE_ERROR_INVALID_ADV_HANDLE Advertising handle not found. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. + */ +SVCALL(SD_BLE_GAP_TX_POWER_SET, uint32_t, sd_ble_gap_tx_power_set(uint8_t role, uint16_t handle, int8_t tx_power)); + +/**@brief Set GAP Appearance value. + * + * @param[in] appearance Appearance (16-bit), see @ref BLE_APPEARANCES. + * + * @retval ::NRF_SUCCESS Appearance value set successfully. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. + */ +SVCALL(SD_BLE_GAP_APPEARANCE_SET, uint32_t, sd_ble_gap_appearance_set(uint16_t appearance)); + +/**@brief Get GAP Appearance value. + * + * @param[out] p_appearance Pointer to appearance (16-bit) to be filled in, see @ref BLE_APPEARANCES. + * + * @retval ::NRF_SUCCESS Appearance value retrieved successfully. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + */ +SVCALL(SD_BLE_GAP_APPEARANCE_GET, uint32_t, sd_ble_gap_appearance_get(uint16_t *p_appearance)); + +/**@brief Set GAP Peripheral Preferred Connection Parameters. + * + * @param[in] p_conn_params Pointer to a @ref ble_gap_conn_params_t structure with the desired parameters. + * + * @retval ::NRF_SUCCESS Peripheral Preferred Connection Parameters set successfully. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. + * @retval ::NRF_ERROR_NOT_SUPPORTED The characteristic is not included in the Attribute Table, + see @ref ble_gap_cfg_ppcp_incl_cfg_t. + */ +SVCALL(SD_BLE_GAP_PPCP_SET, uint32_t, sd_ble_gap_ppcp_set(ble_gap_conn_params_t const *p_conn_params)); + +/**@brief Get GAP Peripheral Preferred Connection Parameters. + * + * @param[out] p_conn_params Pointer to a @ref ble_gap_conn_params_t structure where the parameters will be stored. + * + * @retval ::NRF_SUCCESS Peripheral Preferred Connection Parameters retrieved successfully. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_NOT_SUPPORTED The characteristic is not included in the Attribute Table, + see @ref ble_gap_cfg_ppcp_incl_cfg_t. + */ +SVCALL(SD_BLE_GAP_PPCP_GET, uint32_t, sd_ble_gap_ppcp_get(ble_gap_conn_params_t *p_conn_params)); + +/**@brief Set GAP device name. + * + * @note If the device name is located in application flash memory (see @ref ble_gap_cfg_device_name_t), + * it cannot be changed. Then @ref NRF_ERROR_FORBIDDEN will be returned. + * + * @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). + * + * @retval ::NRF_SUCCESS GAP device name and permissions set successfully. + * @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_FORBIDDEN Device name is not writable. + */ +SVCALL(SD_BLE_GAP_DEVICE_NAME_SET, uint32_t, + sd_ble_gap_device_name_set(ble_gap_conn_sec_mode_t const *p_write_perm, uint8_t const *p_dev_name, uint16_t len)); + +/**@brief Get GAP device name. + * + * @note If the device name is longer than the size of the supplied buffer, + * p_len will return the complete device name length, + * 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[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. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_DATA_SIZE Invalid data size(s) supplied. + */ +SVCALL(SD_BLE_GAP_DEVICE_NAME_GET, uint32_t, sd_ble_gap_device_name_get(uint8_t *p_dev_name, uint16_t *p_len)); + +/**@brief Initiate the GAP Authentication procedure. + * + * @details In the central role, this function will send an SMP Pairing Request (or an SMP Pairing Failed if rejected), + * otherwise in the peripheral role, an SMP Security Request will be sent. + * + * @events + * @event{Depending on the security parameters set and the packet exchanges with the peer\, the following events may be + * generated:} + * @event{@ref BLE_GAP_EVT_SEC_PARAMS_REQUEST} + * @event{@ref BLE_GAP_EVT_SEC_INFO_REQUEST} + * @event{@ref BLE_GAP_EVT_PASSKEY_DISPLAY} + * @event{@ref BLE_GAP_EVT_KEY_PRESSED} + * @event{@ref BLE_GAP_EVT_AUTH_KEY_REQUEST} + * @event{@ref BLE_GAP_EVT_LESC_DHKEY_REQUEST} + * @event{@ref BLE_GAP_EVT_CONN_SEC_UPDATE} + * @event{@ref BLE_GAP_EVT_AUTH_STATUS} + * @event{@ref BLE_GAP_EVT_TIMEOUT} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_GAP_PERIPH_SEC_REQ_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_SEC_REQ_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_ENC_AUTH_MUTEX_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_PAIRING_JW_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_BONDING_JW_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_BONDING_PK_PERIPH_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_BONDING_PK_PERIPH_OOB_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_LESC_PAIRING_JW_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_LESC_BONDING_NC_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_LESC_BONDING_PKE_PD_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_LESC_BONDING_PKE_CD_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_LESC_BONDING_OOB_MSC} + * @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. + * + * @retval ::NRF_SUCCESS Successfully initiated authentication procedure. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. + * @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 ::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 + * Address Resolution characteristic is configured to be included or + * the Softdevice is configured to support peripheral roles only. + * 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)); + +/**@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. + * + * @events + * @event{This function is used during authentication procedures, see the list of events in the documentation of @ref + * sd_ble_gap_authenticate.} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_GAP_PERIPH_PAIRING_JW_MSC} + * @mmsc{@ref BLE_GAP_PERIPH_BONDING_JW_MSC} + * @mmsc{@ref BLE_GAP_PERIPH_BONDING_PK_PERIPH_MSC} + * @mmsc{@ref BLE_GAP_PERIPH_BONDING_PK_CENTRAL_OOB_MSC} + * @mmsc{@ref BLE_GAP_PERIPH_BONDING_STATIC_PK_MSC} + * @mmsc{@ref BLE_GAP_PERIPH_PAIRING_CONFIRM_FAIL_MSC} + * @mmsc{@ref BLE_GAP_PERIPH_LESC_PAIRING_JW_MSC} + * @mmsc{@ref BLE_GAP_PERIPH_LESC_BONDING_NC_MSC} + * @mmsc{@ref BLE_GAP_PERIPH_LESC_BONDING_PKE_PD_MSC} + * @mmsc{@ref BLE_GAP_PERIPH_LESC_BONDING_PKE_CD_MSC} + * @mmsc{@ref BLE_GAP_PERIPH_LESC_BONDING_OOB_MSC} + * @mmsc{@ref BLE_GAP_PERIPH_PAIRING_KS_TOO_SMALL_MSC} + * @mmsc{@ref BLE_GAP_PERIPH_PAIRING_APP_ERROR_MSC} + * @mmsc{@ref BLE_GAP_PERIPH_PAIRING_REMOTE_PAIRING_FAIL_MSC} + * @mmsc{@ref BLE_GAP_PERIPH_PAIRING_TIMEOUT_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_PAIRING_JW_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_BONDING_JW_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_BONDING_PK_PERIPH_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_BONDING_PK_PERIPH_OOB_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_LESC_PAIRING_JW_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_LESC_BONDING_NC_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_LESC_BONDING_PKE_PD_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_LESC_BONDING_PKE_CD_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_LESC_BONDING_OOB_MSC} + * @endmscs + * + * @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 + * @ref BLE_GAP_EVT_AUTH_STATUS event. + * + * @retval ::NRF_SUCCESS Successfully accepted security parameter from the application. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_BUSY The stack is busy, process pending events and retry. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. + * @retval ::NRF_ERROR_INVALID_STATE Security parameters has not been requested. + * @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 + * Address Resolution characteristic is configured to be included or + * the Softdevice is configured to support peripheral roles only. + * See @ref ble_gap_cfg_car_incl_cfg_t and @ref ble_gap_cfg_role_count_t. + */ +SVCALL(SD_BLE_GAP_SEC_PARAMS_REPLY, uint32_t, + sd_ble_gap_sec_params_reply(uint16_t conn_handle, uint8_t sec_status, ble_gap_sec_params_t const *p_sec_params, + ble_gap_sec_keyset_t const *p_sec_keyset)); + +/**@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. + * + * @events + * @event{This function is used during authentication procedures\, see the list of events in the documentation of @ref + * sd_ble_gap_authenticate.} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_GAP_PERIPH_BONDING_PK_CENTRAL_OOB_MSC} + * @mmsc{@ref BLE_GAP_PERIPH_LESC_BONDING_NC_MSC} + * @mmsc{@ref BLE_GAP_PERIPH_LESC_BONDING_PKE_CD_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_BONDING_PK_PERIPH_OOB_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_LESC_BONDING_NC_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_LESC_BONDING_PKE_CD_MSC} + * @endmscs + * + * @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. + * + * @retval ::NRF_SUCCESS Authentication key successfully set. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. + * @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)); + +/**@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. + * + * @events + * @event{This function is used during authentication procedures\, see the list of events in the documentation of @ref + * sd_ble_gap_authenticate.} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_GAP_PERIPH_LESC_PAIRING_JW_MSC} + * @mmsc{@ref BLE_GAP_PERIPH_LESC_BONDING_NC_MSC} + * @mmsc{@ref BLE_GAP_PERIPH_LESC_BONDING_PKE_PD_MSC} + * @mmsc{@ref BLE_GAP_PERIPH_LESC_BONDING_PKE_CD_MSC} + * @mmsc{@ref BLE_GAP_PERIPH_LESC_BONDING_OOB_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_LESC_PAIRING_JW_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_LESC_BONDING_NC_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_LESC_BONDING_PKE_PD_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_LESC_BONDING_PKE_CD_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_LESC_BONDING_OOB_MSC} + * @endmscs + * + * @param[in] conn_handle Connection handle. + * @param[in] p_dhkey LE Secure Connections DHKey. + * + * @retval ::NRF_SUCCESS DHKey successfully set. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. + * @retval ::NRF_ERROR_INVALID_STATE Invalid state to perform operation. Either: + * - The peer is not authenticated. + * - 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)); + +/**@brief Notify the peer of a local keypress. + * + * @mscs + * @mmsc{@ref BLE_GAP_PERIPH_LESC_BONDING_PKE_CD_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_LESC_BONDING_PKE_CD_MSC} + * @endmscs + * + * @param[in] conn_handle Connection handle. + * @param[in] kp_not See @ref BLE_GAP_KP_NOT_TYPES. + * + * @retval ::NRF_SUCCESS Keypress notification successfully queued for transmission. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. + * @retval ::NRF_ERROR_INVALID_STATE Invalid state to perform operation. Either: + * - Authentication key not requested. + * - Passkey has not been entered. + * - Keypresses have not been enabled by both peers. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. + * @retval ::NRF_ERROR_BUSY The BLE stack is busy. Retry at later time. + */ +SVCALL(SD_BLE_GAP_KEYPRESS_NOTIFY, uint32_t, sd_ble_gap_keypress_notify(uint16_t conn_handle, uint8_t kp_not)); + +/**@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. + * + * @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] 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. + * + * @retval ::NRF_SUCCESS OOB data successfully generated. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @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)); + +/**@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. + * + * @events + * @event{This function is used during authentication procedures\, see the list of events in the documentation of @ref + * sd_ble_gap_authenticate.} + * @endevents + * + * @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. + * @param[in] p_oobd_own The OOB data sent out of band to a peer or NULL if the peer has not received OOB data. + * Must correspond to @ref ble_gap_sec_params_t::oob flag in @ref BLE_GAP_EVT_SEC_PARAMS_REQUEST. + * @param[in] p_oobd_peer The OOB data received out of band from a peer or NULL if none received. + * Must correspond to @ref ble_gap_sec_params_t::oob flag + * in @ref sd_ble_gap_authenticate in the central role or + * in @ref sd_ble_gap_sec_params_reply in the peripheral role. + * + * @retval ::NRF_SUCCESS OOB data accepted. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_INVALID_STATE Invalid state to perform operation. Either: + * - Authentication key not requested + * - Not expecting LESC OOB data + * - Have not actually exchanged passkeys. + * @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)); + +/**@brief Initiate GAP Encryption procedure. + * + * @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.} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_GAP_CENTRAL_ENC_AUTH_MUTEX_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_ENC_MSC} + * @mmsc{@ref BLE_GAP_MULTILINK_CTRL_PROC_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_SEC_REQ_MSC} + * @endmscs + * + * @param[in] conn_handle Connection handle. + * @param[in] p_master_id Pointer to a @ref ble_gap_master_id_t master identification structure. + * @param[in] p_enc_info Pointer to a @ref ble_gap_enc_info_t encryption information structure. + * + * @retval ::NRF_SUCCESS Successfully initiated authentication procedure. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @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. + */ +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 + * @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 Data signing is not yet supported, and p_sign_info must therefore be NULL. + * + * @mscs + * @mmsc{@ref BLE_GAP_PERIPH_ENC_MSC} + * @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 + * available. + * + * @retval ::NRF_SUCCESS Successfully accepted security information. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. + * @retval ::NRF_ERROR_INVALID_STATE Invalid state to perform operation. Either: + * - No link has been established. + * - No @ref BLE_GAP_EVT_SEC_INFO_REQUEST pending. + * - Encryption information provided by the app without being requested. See @ref + * ble_gap_evt_sec_info_request_t::enc_info. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. + */ +SVCALL(SD_BLE_GAP_SEC_INFO_REPLY, uint32_t, + sd_ble_gap_sec_info_reply(uint16_t conn_handle, ble_gap_enc_info_t const *p_enc_info, ble_gap_irk_t const *p_id_info, + ble_gap_sign_info_t const *p_sign_info)); + +/**@brief Get the current connection security. + * + * @param[in] conn_handle Connection handle. + * @param[out] p_conn_sec Pointer to a @ref ble_gap_conn_sec_t structure to be filled in. + * + * @retval ::NRF_SUCCESS Current connection security successfully retrieved. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. + */ +SVCALL(SD_BLE_GAP_CONN_SEC_GET, uint32_t, sd_ble_gap_conn_sec_get(uint16_t conn_handle, ble_gap_conn_sec_t *p_conn_sec)); + +/**@brief Start reporting the received signal strength to the application. + * + * A new event is reported whenever the RSSI value changes, until @ref sd_ble_gap_rssi_stop is called. + * + * @events + * @event{@ref BLE_GAP_EVT_RSSI_CHANGED, New RSSI data available. How often the event is generated is + * dependent on the settings of the threshold_dbm + * and skip_count input parameters.} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_GAP_CENTRAL_RSSI_READ_MSC} + * @mmsc{@ref BLE_GAP_RSSI_FILT_MSC} + * @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] 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. + * + * @retval ::NRF_SUCCESS Successfully activated RSSI reporting. + * @retval ::NRF_ERROR_INVALID_STATE RSSI reporting is already ongoing. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. + */ +SVCALL(SD_BLE_GAP_RSSI_START, uint32_t, sd_ble_gap_rssi_start(uint16_t conn_handle, uint8_t threshold_dbm, uint8_t skip_count)); + +/**@brief Stop reporting the received signal strength. + * + * @note An RSSI change detected before the call but not yet received by the application + * may be reported after @ref sd_ble_gap_rssi_stop has been called. + * + * @mscs + * @mmsc{@ref BLE_GAP_CENTRAL_RSSI_READ_MSC} + * @mmsc{@ref BLE_GAP_RSSI_FILT_MSC} + * @endmscs + * + * @param[in] conn_handle Connection handle. + * + * @retval ::NRF_SUCCESS Successfully deactivated RSSI reporting. + * @retval ::NRF_ERROR_INVALID_STATE RSSI reporting is not ongoing. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. + */ +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. + * @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} + * @endmscs + * + * @param[in] conn_handle Connection handle. + * @param[out] p_rssi Pointer to the location where the RSSI measurement shall be stored. + * @param[out] p_ch_index Pointer to the location where Channel Index for the RSSI measurement shall be stored. + * + * @retval ::NRF_SUCCESS Successfully read the RSSI. + * @retval ::NRF_ERROR_NOT_FOUND No sample is available. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. + * @retval ::NRF_ERROR_INVALID_STATE RSSI reporting is not ongoing. + */ +SVCALL(SD_BLE_GAP_RSSI_GET, uint32_t, sd_ble_gap_rssi_get(uint16_t conn_handle, int8_t *p_rssi, uint8_t *p_ch_index)); + +/**@brief Start or continue scanning (GAP Discovery procedure, Observer Procedure). + * + * @note A call to this function will require the application to keep the memory pointed by + * p_adv_report_buffer alive until the buffer is released. The buffer is released when the scanner is stopped + * or when this function is called with another buffer. + * + * @note The scanner will automatically stop in the following cases: + * - @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 + * 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. + * + * @events + * @event{@ref BLE_GAP_EVT_ADV_REPORT, An advertising or scan response packet has been received.} + * @event{@ref BLE_GAP_EVT_TIMEOUT, Scanner has timed out.} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_GAP_SCAN_MSC} + * @mmsc{@ref BLE_GAP_WL_SHARE_MSC} + * @endmscs + * + * @param[in] p_scan_params Pointer to scan parameters structure. When this function is used to continue + * scanning, this parameter must be NULL. + * @param[in] p_adv_report_buffer Pointer to buffer used to store incoming advertising data. + * 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. + * + * @retval ::NRF_SUCCESS Successfully initiated scanning procedure. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_INVALID_STATE Invalid state to perform operation. Either: + * - Scanning is already ongoing and p_scan_params was not NULL + * - Scanning is not running and p_scan_params was NULL. + * - The scanner has timed out when this function is called to continue scanning. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. See @ref ble_gap_scan_params_t. + * @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 + */ +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). + * + * @note The buffer provided in @ref sd_ble_gap_scan_start is released. + * + * @mscs + * @mmsc{@ref BLE_GAP_SCAN_MSC} + * @mmsc{@ref BLE_GAP_WL_SHARE_MSC} + * @endmscs + * + * @retval ::NRF_SUCCESS Successfully stopped scanning procedure. + * @retval ::NRF_ERROR_INVALID_STATE Not in the scanning state. + */ +SVCALL(SD_BLE_GAP_SCAN_STOP, uint32_t, sd_ble_gap_scan_stop(void)); + +/**@brief Create a connection (GAP Link Establishment). + * + * @note If a scanning procedure is currently in progress it will be automatically stopped when calling this function. + * The scanning procedure will be stopped even if the function returns an error. + * + * @events + * @event{@ref BLE_GAP_EVT_CONNECTED, A connection was established.} + * @event{@ref BLE_GAP_EVT_TIMEOUT, Failed to establish a connection.} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_GAP_WL_SHARE_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_CONN_PRIV_MSC} + * @mmsc{@ref BLE_GAP_CENTRAL_CONN_MSC} + * @endmscs + * + * @param[in] p_peer_addr Pointer to peer identity address. If @ref ble_gap_scan_params_t::filter_policy is set to use + * whitelist, then p_peer_addr is ignored. + * @param[in] p_scan_params Pointer to scan parameters structure. + * @param[in] p_conn_params Pointer to desired connection parameters. + * @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. + * + * @retval ::NRF_SUCCESS Successfully initiated connection procedure. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid parameter(s) pointer supplied. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. + * - Invalid parameter(s) in p_scan_params or p_conn_params. + * - Use of whitelist requested but whitelist has not been set, see @ref + * sd_ble_gap_whitelist_set. + * - 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 ::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_RESOURCES Either: + * - Not enough BLE role slots available. + * 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)); + +/**@brief Cancel a connection establishment. + * + * @mscs + * @mmsc{@ref BLE_GAP_CENTRAL_CONN_MSC} + * @endmscs + * + * @retval ::NRF_SUCCESS Successfully canceled an ongoing connection procedure. + * @retval ::NRF_ERROR_INVALID_STATE No locally initiated connect procedure started or connection + * completed occurred. + */ +SVCALL(SD_BLE_GAP_CONNECT_CANCEL, uint32_t, sd_ble_gap_connect_cancel(void)); + +/**@brief Initiate or respond to a PHY Update Procedure + * + * @details This function is used to initiate or respond to a PHY Update Procedure. It will always + * generate a @ref BLE_GAP_EVT_PHY_UPDATE event if successfully executed. + * If this function is used to initiate a PHY Update procedure and the only option + * provided in @ref ble_gap_phys_t::tx_phys and @ref ble_gap_phys_t::rx_phys is the + * currently active PHYs in the respective directions, the SoftDevice will generate a + * @ref BLE_GAP_EVT_PHY_UPDATE with the current PHYs set and will not initiate the + * procedure in the Link Layer. + * + * If @ref ble_gap_phys_t::tx_phys or @ref ble_gap_phys_t::rx_phys is @ref BLE_GAP_PHY_AUTO, + * then the stack will select PHYs based on the peer's PHY preferences and the local link + * configuration. The PHY Update procedure will for this case result in a PHY combination + * that respects the time constraints configured with @ref sd_ble_cfg_set and the current + * link layer data length. + * + * When acting as a central, the SoftDevice will select the fastest common PHY in each direction. + * + * If the peer does not support the PHY Update Procedure, then the resulting + * @ref BLE_GAP_EVT_PHY_UPDATE event will have a status set to + * @ref BLE_HCI_UNSUPPORTED_REMOTE_FEATURE. + * + * If the PHY Update procedure was rejected by the peer due to a procedure collision, the status + * will be @ref BLE_HCI_STATUS_CODE_LMP_ERROR_TRANSACTION_COLLISION or + * @ref BLE_HCI_DIFFERENT_TRANSACTION_COLLISION. + * If the peer responds to the PHY Update procedure with invalid parameters, the status + * will be @ref BLE_HCI_STATUS_CODE_INVALID_LMP_PARAMETERS. + * If the PHY Update procedure was rejected by the peer for a different reason, the status will + * contain the reason as specified by the peer. + * + * @events + * @event{@ref BLE_GAP_EVT_PHY_UPDATE, Result of the PHY Update Procedure.} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_GAP_CENTRAL_PHY_UPDATE} + * @mmsc{@ref BLE_GAP_PERIPHERAL_PHY_UPDATE} + * @endmscs + * + * @param[in] conn_handle Connection handle to indicate the connection for which the PHY Update is requested. + * @param[in] p_gap_phys Pointer to PHY structure. + * + * @retval ::NRF_SUCCESS Successfully requested a PHY Update. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @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 + * @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. + * + */ +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)); + +/**@brief Initiate or respond to a Data Length Update Procedure. + * + * @note If the application uses @ref BLE_GAP_DATA_LENGTH_AUTO for one or more members of + * p_dl_params, the SoftDevice will choose the highest value supported in current + * configuration and connection parameters. + * @note If the link PHY is Coded, the SoftDevice will ensure that the MaxTxTime and/or MaxRxTime + * used in the Data Length Update procedure is at least 2704 us. Otherwise, MaxTxTime and + * MaxRxTime will be limited to maximum 2120 us. + * + * @param[in] conn_handle Connection handle. + * @param[in] p_dl_params Pointer to local parameters to be used in Data Length Update + * Procedure. Set any member to @ref BLE_GAP_DATA_LENGTH_AUTO to let + * the SoftDevice automatically decide the value for that member. + * Set to NULL to use automatic values for all members. + * @param[out] p_dl_limitation Pointer to limitation to be written when local device does not + * have enough resources or does not support the requested Data Length + * Update parameters. Ignored if NULL. + * + * @mscs + * @mmsc{@ref BLE_GAP_DATA_LENGTH_UPDATE_PROCEDURE_MSC} + * @endmscs + * + * @retval ::NRF_SUCCESS Successfully set Data Length Extension initiation/response parameters. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle parameter supplied. + * @retval ::NRF_ERROR_INVALID_STATE No link has been established. + * @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_BUSY Peer has already initiated a Data Length Update Procedure. Process the + * pending @ref BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST event to respond. + */ +SVCALL(SD_BLE_GAP_DATA_LENGTH_UPDATE, uint32_t, + sd_ble_gap_data_length_update(uint16_t conn_handle, ble_gap_data_length_params_t const *p_dl_params, + ble_gap_data_length_limitation_t *p_dl_limitation)); + +/**@brief Start the Quality of Service (QoS) channel survey module. + * + * @details The channel survey module provides measurements of the energy levels on + * the Bluetooth Low Energy channels. When the module is enabled, @ref BLE_GAP_EVT_QOS_CHANNEL_SURVEY_REPORT + * events will periodically report the measured energy levels for each channel. + * + * @note The measurements are scheduled with lower priority than other Bluetooth Low Energy roles, + * Radio Timeslot API events and Flash API events. + * + * @note The channel survey module will attempt to do measurements so that the average interval + * between measurements will be interval_us. However due to the channel survey module + * having the lowest priority of all roles and modules, this may not be possible. In that + * case fewer than expected channel survey reports may be given. + * + * @note In order to use the channel survey module, @ref ble_gap_cfg_role_count_t::qos_channel_survey_role_available + * must be set. This is done using @ref sd_ble_cfg_set. + * + * @param[in] interval_us Requested average interval for the measurements and reports. See + * @ref BLE_GAP_QOS_CHANNEL_SURVEY_INTERVALS for valid ranges. If set + * to @ref BLE_GAP_QOS_CHANNEL_SURVEY_INTERVAL_CONTINUOUS, the channel + * survey role will be scheduled at every available opportunity. + * + * @retval ::NRF_SUCCESS The module is successfully started. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter supplied. interval_us is out of the + * allowed range. + * @retval ::NRF_ERROR_INVALID_STATE Trying to start the module when already running. + * @retval ::NRF_ERROR_RESOURCES The channel survey module is not available to the application. + * Set @ref ble_gap_cfg_role_count_t::qos_channel_survey_role_available using + * @ref sd_ble_cfg_set. + */ +SVCALL(SD_BLE_GAP_QOS_CHANNEL_SURVEY_START, uint32_t, sd_ble_gap_qos_channel_survey_start(uint32_t interval_us)); + +/**@brief Stop the Quality of Service (QoS) channel survey module. + * + * @note The SoftDevice may generate one @ref BLE_GAP_EVT_QOS_CHANNEL_SURVEY_REPORT event after this + * function is called. + * + * @retval ::NRF_SUCCESS The module is successfully stopped. + * @retval ::NRF_ERROR_INVALID_STATE Trying to stop the module when it is not running. + */ +SVCALL(SD_BLE_GAP_QOS_CHANNEL_SURVEY_STOP, uint32_t, sd_ble_gap_qos_channel_survey_stop(void)); + +/**@brief Obtain the next connection event counter value. + * + * @details The connection event counter is initialized to zero on the first connection event. The value is incremented + * by one for each connection event. For more information see Bluetooth Core Specification v5.0, Vol 6, Part B, + * Section 4.5.1. + * + * @note The connection event counter obtained through this API will be outdated if this API is called + * at the same time as the connection event counter is incremented. + * + * @note This API will always return the last connection event counter + 1. + * The actual connection event may be multiple connection events later if: + * - Slave latency is enabled and there is no data to transmit or receive. + * - Another role is scheduled with a higher priority at the same time as the next connection event. + * + * @param[in] conn_handle Connection handle. + * @param[out] p_counter Pointer to the variable where the next connection event counter will be written. + * + * @retval ::NRF_SUCCESS The connection event counter was successfully retrieved. + * @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)); + +/**@brief Start triggering a given task on connection event start. + * + * @details When enabled, this feature will trigger a PPI task at the start of connection events. + * The application can configure the SoftDevice to trigger every N connection events starting from + * a given connection event counter. See also @ref ble_gap_conn_event_trigger_t. + * + * @param[in] conn_handle Connection handle. + * @param[in] p_params Connection event trigger parameters. + * + * @retval ::NRF_SUCCESS Success. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter supplied. See @ref ble_gap_conn_event_trigger_t. + * @retval ::NRF_ERROR_INVALID_STATE Either: + * - Trying to start connection event triggering when it is already ongoing. + * - @ref ble_gap_conn_event_trigger_t::conn_evt_counter_start is in the past. + * Use @ref sd_ble_gap_next_conn_evt_counter_get to find a new value + to be used as ble_gap_conn_event_trigger_t::conn_evt_counter_start. + */ +SVCALL(SD_BLE_GAP_CONN_EVT_TRIGGER_START, uint32_t, + sd_ble_gap_conn_evt_trigger_start(uint16_t conn_handle, ble_gap_conn_event_trigger_t const *p_params)); + +/**@brief Stop triggering the task configured using @ref sd_ble_gap_conn_evt_trigger_start. + * + * @param[in] conn_handle Connection handle. + * + * @retval ::NRF_SUCCESS Success. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. + * @retval ::NRF_ERROR_INVALID_STATE Trying to stop connection event triggering when it is not enabled. + */ +SVCALL(SD_BLE_GAP_CONN_EVT_TRIGGER_STOP, uint32_t, sd_ble_gap_conn_evt_trigger_stop(uint16_t conn_handle)); + +/** @} */ + +#ifdef __cplusplus +} +#endif +#endif // BLE_GAP_H__ + +/** + @} +*/ diff --git a/src/platform/nrf52/softdevice/ble_gatt.h b/src/platform/nrf52/softdevice/ble_gatt.h new file mode 100644 index 0000000..df0d728 --- /dev/null +++ b/src/platform/nrf52/softdevice/ble_gatt.h @@ -0,0 +1,232 @@ +/* + * Copyright (c) Nordic Semiconductor ASA + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form, except as embedded into a Nordic + * Semiconductor ASA integrated circuit in a product or a software update for + * such product, must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of Nordic Semiconductor ASA nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * 4. This software, with or without modification, must only be used with a + * Nordic Semiconductor ASA integrated circuit. + * + * 5. Any software provided in binary form under this license must not be reverse + * engineered, decompiled, modified and/or disassembled. + * + * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + @addtogroup BLE_GATT Generic Attribute Profile (GATT) Common + @{ + @brief Common definitions and prototypes for the GATT interfaces. + */ + +#ifndef BLE_GATT_H__ +#define BLE_GATT_H__ + +#include "ble_err.h" +#include "ble_hci.h" +#include "ble_ranges.h" +#include "ble_types.h" +#include "nrf_error.h" +#include "nrf_svc.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @addtogroup BLE_GATT_DEFINES Defines + * @{ */ + +/** @brief Default ATT MTU, in bytes. */ +#define BLE_GATT_ATT_MTU_DEFAULT 23 + +/**@brief Invalid Attribute Handle. */ +#define BLE_GATT_HANDLE_INVALID 0x0000 + +/**@brief First Attribute Handle. */ +#define BLE_GATT_HANDLE_START 0x0001 + +/**@brief Last Attribute Handle. */ +#define BLE_GATT_HANDLE_END 0xFFFF + +/** @defgroup BLE_GATT_TIMEOUT_SOURCES GATT Timeout sources + * @{ */ +#define BLE_GATT_TIMEOUT_SRC_PROTOCOL 0x00 /**< ATT Protocol timeout. */ +/** @} */ + +/** @defgroup BLE_GATT_WRITE_OPS GATT Write operations + * @{ */ +#define BLE_GATT_OP_INVALID 0x00 /**< Invalid Operation. */ +#define BLE_GATT_OP_WRITE_REQ 0x01 /**< Write Request. */ +#define BLE_GATT_OP_WRITE_CMD 0x02 /**< Write Command. */ +#define BLE_GATT_OP_SIGN_WRITE_CMD 0x03 /**< Signed Write Command. */ +#define BLE_GATT_OP_PREP_WRITE_REQ 0x04 /**< Prepare Write Request. */ +#define BLE_GATT_OP_EXEC_WRITE_REQ 0x05 /**< Execute Write Request. */ +/** @} */ + +/** @defgroup BLE_GATT_EXEC_WRITE_FLAGS GATT Execute Write flags + * @{ */ +#define BLE_GATT_EXEC_WRITE_FLAG_PREPARED_CANCEL 0x00 /**< Cancel prepared write. */ +#define BLE_GATT_EXEC_WRITE_FLAG_PREPARED_WRITE 0x01 /**< Execute prepared write. */ +/** @} */ + +/** @defgroup BLE_GATT_HVX_TYPES GATT Handle Value operations + * @{ */ +#define BLE_GATT_HVX_INVALID 0x00 /**< Invalid Operation. */ +#define BLE_GATT_HVX_NOTIFICATION 0x01 /**< Handle Value Notification. */ +#define BLE_GATT_HVX_INDICATION 0x02 /**< Handle Value Indication. */ +/** @} */ + +/** @defgroup BLE_GATT_STATUS_CODES GATT Status Codes + * @{ */ +#define BLE_GATT_STATUS_SUCCESS 0x0000 /**< Success. */ +#define BLE_GATT_STATUS_UNKNOWN 0x0001 /**< Unknown or not applicable status. */ +#define BLE_GATT_STATUS_ATTERR_INVALID 0x0100 /**< ATT Error: Invalid Error Code. */ +#define BLE_GATT_STATUS_ATTERR_INVALID_HANDLE 0x0101 /**< ATT Error: Invalid Attribute Handle. */ +#define BLE_GATT_STATUS_ATTERR_READ_NOT_PERMITTED 0x0102 /**< ATT Error: Read not permitted. */ +#define BLE_GATT_STATUS_ATTERR_WRITE_NOT_PERMITTED 0x0103 /**< ATT Error: Write not permitted. */ +#define BLE_GATT_STATUS_ATTERR_INVALID_PDU 0x0104 /**< ATT Error: Used in ATT as Invalid PDU. */ +#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_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. */ +/** @} */ + +/** @defgroup BLE_GATT_CPF_FORMATS Characteristic Presentation Formats + * @note Found at + * http://developer.bluetooth.org/gatt/descriptors/Pages/DescriptorViewer.aspx?u=org.bluetooth.descriptor.gatt.characteristic_presentation_format.xml + * @{ */ +#define BLE_GATT_CPF_FORMAT_RFU 0x00 /**< Reserved For Future Use. */ +#define BLE_GATT_CPF_FORMAT_BOOLEAN 0x01 /**< Boolean. */ +#define BLE_GATT_CPF_FORMAT_2BIT 0x02 /**< Unsigned 2-bit integer. */ +#define BLE_GATT_CPF_FORMAT_NIBBLE 0x03 /**< Unsigned 4-bit integer. */ +#define BLE_GATT_CPF_FORMAT_UINT8 0x04 /**< Unsigned 8-bit integer. */ +#define BLE_GATT_CPF_FORMAT_UINT12 0x05 /**< Unsigned 12-bit integer. */ +#define BLE_GATT_CPF_FORMAT_UINT16 0x06 /**< Unsigned 16-bit integer. */ +#define BLE_GATT_CPF_FORMAT_UINT24 0x07 /**< Unsigned 24-bit integer. */ +#define BLE_GATT_CPF_FORMAT_UINT32 0x08 /**< Unsigned 32-bit integer. */ +#define BLE_GATT_CPF_FORMAT_UINT48 0x09 /**< Unsigned 48-bit integer. */ +#define BLE_GATT_CPF_FORMAT_UINT64 0x0A /**< Unsigned 64-bit integer. */ +#define BLE_GATT_CPF_FORMAT_UINT128 0x0B /**< Unsigned 128-bit integer. */ +#define BLE_GATT_CPF_FORMAT_SINT8 0x0C /**< Signed 2-bit integer. */ +#define BLE_GATT_CPF_FORMAT_SINT12 0x0D /**< Signed 12-bit integer. */ +#define BLE_GATT_CPF_FORMAT_SINT16 0x0E /**< Signed 16-bit integer. */ +#define BLE_GATT_CPF_FORMAT_SINT24 0x0F /**< Signed 24-bit integer. */ +#define BLE_GATT_CPF_FORMAT_SINT32 0x10 /**< Signed 32-bit integer. */ +#define BLE_GATT_CPF_FORMAT_SINT48 0x11 /**< Signed 48-bit integer. */ +#define BLE_GATT_CPF_FORMAT_SINT64 0x12 /**< Signed 64-bit integer. */ +#define BLE_GATT_CPF_FORMAT_SINT128 0x13 /**< Signed 128-bit integer. */ +#define BLE_GATT_CPF_FORMAT_FLOAT32 0x14 /**< IEEE-754 32-bit floating point. */ +#define BLE_GATT_CPF_FORMAT_FLOAT64 0x15 /**< IEEE-754 64-bit floating point. */ +#define BLE_GATT_CPF_FORMAT_SFLOAT 0x16 /**< IEEE-11073 16-bit SFLOAT. */ +#define BLE_GATT_CPF_FORMAT_FLOAT 0x17 /**< IEEE-11073 32-bit FLOAT. */ +#define BLE_GATT_CPF_FORMAT_DUINT16 0x18 /**< IEEE-20601 format. */ +#define BLE_GATT_CPF_FORMAT_UTF8S 0x19 /**< UTF-8 string. */ +#define BLE_GATT_CPF_FORMAT_UTF16S 0x1A /**< UTF-16 string. */ +#define BLE_GATT_CPF_FORMAT_STRUCT 0x1B /**< Opaque Structure. */ +/** @} */ + +/** @defgroup BLE_GATT_CPF_NAMESPACES GATT Bluetooth Namespaces + * @{ + */ +#define BLE_GATT_CPF_NAMESPACE_BTSIG 0x01 /**< Bluetooth SIG defined Namespace. */ +#define BLE_GATT_CPF_NAMESPACE_DESCRIPTION_UNKNOWN 0x0000 /**< Namespace Description Unknown. */ +/** @} */ + +/** @} */ + +/** @addtogroup BLE_GATT_STRUCTURES Structures + * @{ */ + +/** + * @brief BLE GATT connection configuration parameters, set with @ref sd_ble_cfg_set. + * + * @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 + */ +} 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. */ +} 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. */ +} ble_gatt_char_ext_props_t; + +/** @} */ + +#ifdef __cplusplus +} +#endif +#endif // BLE_GATT_H__ + +/** @} */ diff --git a/src/platform/nrf52/softdevice/ble_gattc.h b/src/platform/nrf52/softdevice/ble_gattc.h new file mode 100644 index 0000000..f1df178 --- /dev/null +++ b/src/platform/nrf52/softdevice/ble_gattc.h @@ -0,0 +1,764 @@ +/* + * Copyright (c) Nordic Semiconductor ASA + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form, except as embedded into a Nordic + * Semiconductor ASA integrated circuit in a product or a software update for + * such product, must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of Nordic Semiconductor ASA nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * 4. This software, with or without modification, must only be used with a + * Nordic Semiconductor ASA integrated circuit. + * + * 5. Any software provided in binary form under this license must not be reverse + * engineered, decompiled, modified and/or disassembled. + * + * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + @addtogroup BLE_GATTC Generic Attribute Profile (GATT) Client + @{ + @brief Definitions and prototypes for the GATT Client interface. + */ + +#ifndef BLE_GATTC_H__ +#define BLE_GATTC_H__ + +#include "ble_err.h" +#include "ble_gatt.h" +#include "ble_ranges.h" +#include "ble_types.h" +#include "nrf.h" +#include "nrf_error.h" +#include "nrf_svc.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @addtogroup BLE_GATTC_ENUMERATIONS Enumerations + * @{ */ + +/**@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. */ +}; + +/** + * @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. */ +}; + +/**@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 */ +}; + +/** @} */ + +/** @addtogroup BLE_GATTC_DEFINES Defines + * @{ */ + +/** @defgroup BLE_ERRORS_GATTC SVC return values specific to GATTC + * @{ */ +#define BLE_ERROR_GATTC_PROC_NOT_PERMITTED (NRF_GATTC_ERR_BASE + 0x000) /**< Procedure not Permitted. */ +/** @} */ + +/** @defgroup BLE_GATTC_ATTR_INFO_FORMAT Attribute Information Formats + * @{ */ +#define BLE_GATTC_ATTR_INFO_FORMAT_16BIT 1 /**< 16-bit Attribute Information Format. */ +#define BLE_GATTC_ATTR_INFO_FORMAT_128BIT 2 /**< 128-bit Attribute Information Format. */ +/** @} */ + +/** @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. */ +/** @} */ + +/** @} */ + +/** @addtogroup BLE_GATTC_STRUCTURES Structures + * @{ */ + +/** + * @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 */ +} ble_gattc_conn_cfg_t; + +/**@brief Operation Handle Range. */ +typedef struct { + 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_gattc_service_t; + +/**@brief GATT include. */ +typedef struct { + 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_gattc_char_t; + +/**@brief GATT descriptor. */ +typedef struct { + 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. */ +} 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. */ +} 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. */ +} 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. */ +} 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. */ +} 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. */ +} 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. */ +} 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. */ +} 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. */ +} 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. */ +} 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. */ +} 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. */ +} 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. */ +} 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. */ +} 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. */ +} 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. */ +} 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. */ +} 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. */ +} ble_gattc_evt_t; + +/**@brief UUID discovery option. + * + * @details Used with @ref sd_ble_opt_set to enable and disable automatic insertion of discovered 128-bit UUIDs to the + * Vendor Specific UUID table. Disabled by default. + * - When disabled, if a procedure initiated by + * @ref sd_ble_gattc_primary_services_discover, + * @ref sd_ble_gattc_relationships_discover, + * @ref sd_ble_gattc_characteristics_discover, + * @ref sd_ble_gattc_descriptors_discover + * finds a 128-bit UUID which was not added by @ref sd_ble_uuid_vs_add, @ref ble_uuid_t::type will be set + * to @ref BLE_UUID_TYPE_UNKNOWN in the corresponding event. + * - When enabled, all found 128-bit UUIDs will be automatically added. The application can use + * @ref sd_ble_uuid_encode to retrieve the 128-bit UUID from @ref ble_uuid_t received in the corresponding + * event. If the total number of Vendor Specific UUIDs exceeds the table capacity, @ref ble_uuid_t::type will + * be set to @ref BLE_UUID_TYPE_UNKNOWN in the corresponding event. + * See also @ref ble_common_cfg_vs_uuid_t, @ref sd_ble_uuid_vs_remove. + * + * @note @ref sd_ble_opt_get is not supported for this option. + * + * @retval ::NRF_SUCCESS Set successfully. + * + */ +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. */ +} 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_t; + +/** @} */ + +/** @addtogroup BLE_GATTC_FUNCTIONS Functions + * @{ */ + +/**@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. + * + * @events + * @event{@ref BLE_GATTC_EVT_PRIM_SRVC_DISC_RSP} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_GATTC_PRIM_SRVC_DISC_MSC} + * @endmscs + * + * @param[in] conn_handle The connection handle identifying the connection to perform this procedure on. + * @param[in] start_handle Handle to start searching from. + * @param[in] p_srvc_uuid Pointer to the service UUID to be found. If it is NULL, all primary services will be returned. + * + * @retval ::NRF_SUCCESS Successfully started or resumed the Primary Service Discovery procedure. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. + * @retval ::NRF_ERROR_INVALID_STATE Invalid Connection State. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. + * @retval ::NRF_ERROR_BUSY Client procedure already in progress. + * @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_PRIMARY_SERVICES_DISCOVER, uint32_t, + sd_ble_gattc_primary_services_discover(uint16_t conn_handle, uint16_t start_handle, ble_uuid_t const *p_srvc_uuid)); + +/**@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 + * ble_gattc_opt_uuid_disc_t. + * + * @events + * @event{@ref BLE_GATTC_EVT_REL_DISC_RSP} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_GATTC_REL_DISC_MSC} + * @endmscs + * + * @param[in] conn_handle The connection handle identifying the connection to perform this procedure on. + * @param[in] p_handle_range A pointer to the range of handles of the Service to perform this procedure on. + * + * @retval ::NRF_SUCCESS Successfully started or resumed the Relationship Discovery procedure. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. + * @retval ::NRF_ERROR_INVALID_STATE Invalid Connection State. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. + * @retval ::NRF_ERROR_BUSY Client procedure already in progress. + * @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_RELATIONSHIPS_DISCOVER, uint32_t, + sd_ble_gattc_relationships_discover(uint16_t conn_handle, ble_gattc_handle_range_t const *p_handle_range)); + +/**@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 + * ble_gattc_opt_uuid_disc_t. + * + * @events + * @event{@ref BLE_GATTC_EVT_CHAR_DISC_RSP} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_GATTC_CHAR_DISC_MSC} + * @endmscs + * + * @param[in] conn_handle The connection handle identifying the connection to perform this procedure on. + * @param[in] p_handle_range A pointer to the range of handles of the Service to perform this procedure on. + * + * @retval ::NRF_SUCCESS Successfully started or resumed the Characteristic Discovery procedure. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. + * @retval ::NRF_ERROR_INVALID_STATE Invalid Connection State. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_BUSY Client procedure already in progress. + * @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_CHARACTERISTICS_DISCOVER, uint32_t, + sd_ble_gattc_characteristics_discover(uint16_t conn_handle, ble_gattc_handle_range_t const *p_handle_range)); + +/**@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 + * ble_gattc_opt_uuid_disc_t. + * + * @events + * @event{@ref BLE_GATTC_EVT_DESC_DISC_RSP} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_GATTC_DESC_DISC_MSC} + * @endmscs + * + * @param[in] conn_handle The connection handle identifying the connection to perform this procedure on. + * @param[in] p_handle_range A pointer to the range of handles of the Characteristic to perform this procedure on. + * + * @retval ::NRF_SUCCESS Successfully started or resumed the Descriptor Discovery procedure. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. + * @retval ::NRF_ERROR_INVALID_STATE Invalid Connection State. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_BUSY Client procedure already in progress. + * @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_DESCRIPTORS_DISCOVER, uint32_t, + sd_ble_gattc_descriptors_discover(uint16_t conn_handle, ble_gattc_handle_range_t const *p_handle_range)); + +/**@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. + * + * @events + * @event{@ref BLE_GATTC_EVT_CHAR_VAL_BY_UUID_READ_RSP} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_GATTC_READ_UUID_MSC} + * @endmscs + * + * @param[in] conn_handle The connection handle identifying the connection to perform this procedure on. + * @param[in] p_uuid Pointer to a Characteristic value UUID to read. + * @param[in] p_handle_range A pointer to the range of handles to perform this procedure on. + * + * @retval ::NRF_SUCCESS Successfully started or resumed the Read using Characteristic UUID procedure. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. + * @retval ::NRF_ERROR_INVALID_STATE Invalid Connection State. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_BUSY Client procedure already in progress. + * @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_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)); + +/**@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. + * + * @events + * @event{@ref BLE_GATTC_EVT_READ_RSP} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_GATTC_VALUE_READ_MSC} + * @endmscs + * + * @param[in] conn_handle The connection handle identifying the connection to perform this procedure on. + * @param[in] handle The handle of the attribute to be read. + * @param[in] offset Offset into the attribute value to be read. + * + * @retval ::NRF_SUCCESS Successfully started or resumed the Read (Long) procedure. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. + * @retval ::NRF_ERROR_INVALID_STATE Invalid Connection State. + * @retval ::NRF_ERROR_BUSY Client procedure already in progress. + * @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_READ, uint32_t, sd_ble_gattc_read(uint16_t conn_handle, uint16_t handle, uint16_t offset)); + +/**@brief Initiate a GATT Read Multiple Characteristic Values procedure. + * + * @details This function initiates a GATT Read Multiple Characteristic Values procedure. + * + * @events + * @event{@ref BLE_GATTC_EVT_CHAR_VALS_READ_RSP} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_GATTC_READ_MULT_MSC} + * @endmscs + * + * @param[in] conn_handle The connection handle identifying the connection to perform this procedure on. + * @param[in] p_handles A pointer to the handle(s) of the attribute(s) to be read. + * @param[in] handle_count The number of handles in p_handles. + * + * @retval ::NRF_SUCCESS Successfully started the Read Multiple Characteristic Values procedure. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. + * @retval ::NRF_ERROR_INVALID_STATE Invalid Connection State. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_BUSY Client procedure already in progress. + * @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_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. + * + * @details This function can perform all write procedures described in GATT. + * + * @note Only one write with response procedure can be ongoing per connection at a time. + * If the application tries to write with response while another write with response procedure is ongoing, + * the function call will return @ref NRF_ERROR_BUSY. + * 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. + * + * @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. + * + * @events + * @event{@ref BLE_GATTC_EVT_WRITE_CMD_TX_COMPLETE, Write without response transmission complete.} + * @event{@ref BLE_GATTC_EVT_WRITE_RSP, Write response received from the peer.} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_GATTC_VALUE_WRITE_WITHOUT_RESP_MSC} + * @mmsc{@ref BLE_GATTC_VALUE_WRITE_MSC} + * @mmsc{@ref BLE_GATTC_VALUE_LONG_WRITE_MSC} + * @mmsc{@ref BLE_GATTC_VALUE_RELIABLE_WRITE_MSC} + * @endmscs + * + * @param[in] conn_handle The connection handle identifying the connection to perform this procedure on. + * @param[in] p_write_params A pointer to a write parameters structure. + * + * @retval ::NRF_SUCCESS Successfully started the Write procedure. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. + * @retval ::NRF_ERROR_INVALID_STATE Invalid Connection State. + * @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_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 + * reestablishing the connection. + */ +SVCALL(SD_BLE_GATTC_WRITE, uint32_t, sd_ble_gattc_write(uint16_t conn_handle, ble_gattc_write_params_t const *p_write_params)); + +/**@brief Send a Handle Value Confirmation to the GATT Server. + * + * @mscs + * @mmsc{@ref BLE_GATTC_HVI_MSC} + * @endmscs + * + * @param[in] conn_handle The connection handle identifying the connection to perform this procedure on. + * @param[in] handle The handle of the attribute in the indication. + * + * @retval ::NRF_SUCCESS Successfully queued the Handle Value Confirmation for transmission. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. + * @retval ::NRF_ERROR_INVALID_STATE Invalid Connection State or no Indication pending to be confirmed. + * @retval ::BLE_ERROR_INVALID_ATTR_HANDLE Invalid attribute handle. + * @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_HV_CONFIRM, uint32_t, sd_ble_gattc_hv_confirm(uint16_t conn_handle, uint16_t handle)); + +/**@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.} + * @endevents + * + * @param[in] conn_handle The connection handle identifying the connection to perform this procedure on. + * @param[in] p_handle_range The range of handles to request information about. + * + * @retval ::NRF_SUCCESS Successfully started an attribute information discovery procedure. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle. + * @retval ::NRF_ERROR_INVALID_STATE Invalid connection state + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_BUSY Client procedure already in progress. + * @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_ATTR_INFO_DISCOVER, uint32_t, + sd_ble_gattc_attr_info_discover(uint16_t conn_handle, ble_gattc_handle_range_t const *p_handle_range)); + +/**@brief Start an ATT_MTU exchange by sending an Exchange MTU Request to the server. + * + * @details The SoftDevice sets ATT_MTU to the minimum of: + * - The Client RX MTU value, and + * - The Server RX MTU value from @ref BLE_GATTC_EVT_EXCHANGE_MTU_RSP. + * + * However, the SoftDevice never sets ATT_MTU lower than @ref BLE_GATT_ATT_MTU_DEFAULT. + * + * @events + * @event{@ref BLE_GATTC_EVT_EXCHANGE_MTU_RSP} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_GATTC_MTU_EXCHANGE} + * @endmscs + * + * @param[in] conn_handle The connection handle identifying the connection to perform this procedure on. + * @param[in] client_rx_mtu Client RX MTU size. + * - 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 + * if an ATT_MTU exchange has already been performed in the other direction. + * + * @retval ::NRF_SUCCESS Successfully sent request to the server. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle. + * @retval ::NRF_ERROR_INVALID_STATE Invalid connection state or an ATT_MTU exchange was already requested once. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid Client RX MTU size supplied. + * @retval ::NRF_ERROR_BUSY Client procedure already in progress. + * @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)); + +/**@brief Iterate through Handle-Value(s) list in @ref BLE_GATTC_EVT_CHAR_VAL_BY_UUID_READ_RSP event. + * + * @param[in] p_gattc_evt Pointer to event buffer containing @ref BLE_GATTC_EVT_CHAR_VAL_BY_UUID_READ_RSP event. + * @note If the buffer contains different event, behavior is undefined. + * @param[in,out] p_iter Iterator, points to @ref ble_gattc_handle_value_t structure that will be filled in with + * the next Handle-Value pair in each iteration. If the function returns other than + * @ref NRF_SUCCESS, it will not be changed. + * - To start iteration, initialize the structure to zero. + * - To continue, pass the value from previous iteration. + * + * \code + * ble_gattc_handle_value_t iter; + * memset(&iter, 0, sizeof(ble_gattc_handle_value_t)); + * while (sd_ble_gattc_evt_char_val_by_uuid_read_rsp_iter(&ble_evt.evt.gattc_evt, &iter) == NRF_SUCCESS) + * { + * app_handle = iter.handle; + * memcpy(app_value, iter.p_value, ble_evt.evt.gattc_evt.params.char_val_by_uuid_read_rsp.value_len); + * } + * \endcode + * + * @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); + +/** @} */ + +#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; + + 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 */ + +#ifdef __cplusplus +} +#endif +#endif /* BLE_GATTC_H__ */ + +/** + @} +*/ diff --git a/src/platform/nrf52/softdevice/ble_gatts.h b/src/platform/nrf52/softdevice/ble_gatts.h new file mode 100644 index 0000000..dc94957 --- /dev/null +++ b/src/platform/nrf52/softdevice/ble_gatts.h @@ -0,0 +1,904 @@ +/* + * Copyright (c) Nordic Semiconductor ASA + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form, except as embedded into a Nordic + * Semiconductor ASA integrated circuit in a product or a software update for + * such product, must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of Nordic Semiconductor ASA nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * 4. This software, with or without modification, must only be used with a + * Nordic Semiconductor ASA integrated circuit. + * + * 5. Any software provided in binary form under this license must not be reverse + * engineered, decompiled, modified and/or disassembled. + * + * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + @addtogroup BLE_GATTS Generic Attribute Profile (GATT) Server + @{ + @brief Definitions and prototypes for the GATTS interface. + */ + +#ifndef BLE_GATTS_H__ +#define BLE_GATTS_H__ + +#include "ble_err.h" +#include "ble_gap.h" +#include "ble_gatt.h" +#include "ble_hci.h" +#include "ble_ranges.h" +#include "ble_types.h" +#include "nrf_error.h" +#include "nrf_svc.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @addtogroup BLE_GATTS_ENUMERATIONS Enumerations + * @{ */ + +/** + * @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. */ +}; + +/** + * @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. */ +}; + +/**@brief GATTS Configuration IDs. + * + * 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. */ +}; + +/** @} */ + +/** @addtogroup BLE_GATTS_DEFINES Defines + * @{ */ + +/** @defgroup BLE_ERRORS_GATTS SVC return values specific to GATTS + * @{ */ +#define BLE_ERROR_GATTS_INVALID_ATTR_TYPE (NRF_GATTS_ERR_BASE + 0x000) /**< Invalid attribute type. */ +#define BLE_ERROR_GATTS_SYS_ATTR_MISSING (NRF_GATTS_ERR_BASE + 0x001) /**< System Attributes missing. */ +/** @} */ + +/** @defgroup BLE_GATTS_ATTR_LENS_MAX Maximum attribute lengths + * @{ */ +#define BLE_GATTS_FIX_ATTR_LEN_MAX (510) /**< Maximum length for fixed length Attribute Values. */ +#define BLE_GATTS_VAR_ATTR_LEN_MAX (512) /**< Maximum length for variable length Attribute Values. */ +/** @} */ + +/** @defgroup BLE_GATTS_SRVC_TYPES GATT Server Service Types + * @{ */ +#define BLE_GATTS_SRVC_TYPE_INVALID 0x00 /**< Invalid Service Type. */ +#define BLE_GATTS_SRVC_TYPE_PRIMARY 0x01 /**< Primary Service. */ +#define BLE_GATTS_SRVC_TYPE_SECONDARY 0x02 /**< Secondary Type. */ +/** @} */ + +/** @defgroup BLE_GATTS_ATTR_TYPES GATT Server Attribute Types + * @{ */ +#define BLE_GATTS_ATTR_TYPE_INVALID 0x00 /**< Invalid Attribute Type. */ +#define BLE_GATTS_ATTR_TYPE_PRIM_SRVC_DECL 0x01 /**< Primary Service Declaration. */ +#define BLE_GATTS_ATTR_TYPE_SEC_SRVC_DECL 0x02 /**< Secondary Service Declaration. */ +#define BLE_GATTS_ATTR_TYPE_INC_DECL 0x03 /**< Include Declaration. */ +#define BLE_GATTS_ATTR_TYPE_CHAR_DECL 0x04 /**< Characteristic Declaration. */ +#define BLE_GATTS_ATTR_TYPE_CHAR_VAL 0x05 /**< Characteristic Value. */ +#define BLE_GATTS_ATTR_TYPE_DESC 0x06 /**< Descriptor. */ +#define BLE_GATTS_ATTR_TYPE_OTHER 0x07 /**< Other, non-GATT specific type. */ +/** @} */ + +/** @defgroup BLE_GATTS_OPS GATT Server Operations + * @{ */ +#define BLE_GATTS_OP_INVALID 0x00 /**< Invalid Operation. */ +#define BLE_GATTS_OP_WRITE_REQ 0x01 /**< Write Request. */ +#define BLE_GATTS_OP_WRITE_CMD 0x02 /**< Write Command. */ +#define BLE_GATTS_OP_SIGN_WRITE_CMD 0x03 /**< Signed Write Command. */ +#define BLE_GATTS_OP_PREP_WRITE_REQ 0x04 /**< Prepare Write Request. */ +#define BLE_GATTS_OP_EXEC_WRITE_REQ_CANCEL 0x05 /**< Execute Write Request: Cancel all prepared writes. */ +#define BLE_GATTS_OP_EXEC_WRITE_REQ_NOW 0x06 /**< Execute Write Request: Immediately execute all prepared writes. */ +/** @} */ + +/** @defgroup BLE_GATTS_VLOCS GATT Value Locations + * @{ */ +#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. */ +/** @} */ + +/** @defgroup BLE_GATTS_AUTHORIZE_TYPES GATT Server Authorization Types + * @{ */ +#define BLE_GATTS_AUTHORIZE_TYPE_INVALID 0x00 /**< Invalid Type. */ +#define BLE_GATTS_AUTHORIZE_TYPE_READ 0x01 /**< Authorize a Read Operation. */ +#define BLE_GATTS_AUTHORIZE_TYPE_WRITE 0x02 /**< Authorize a Write Request Operation. */ +/** @} */ + +/** @defgroup BLE_GATTS_SYS_ATTR_FLAGS System Attribute Flags + * @{ */ +#define BLE_GATTS_SYS_ATTR_FLAG_SYS_SRVCS (1 << 0) /**< Restrict system attributes to system services only. */ +#define BLE_GATTS_SYS_ATTR_FLAG_USR_SRVCS (1 << 1) /**< Restrict system attributes to user services only. */ +/** @} */ + +/** @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. */ +/** @} */ + +/** @defgroup BLE_GATTS_ATTR_TAB_SIZE Attribute Table size + * @{ + */ +#define BLE_GATTS_ATTR_TAB_SIZE_MIN (248) /**< Minimum Attribute Table size */ +#define BLE_GATTS_ATTR_TAB_SIZE_DEFAULT (1408) /**< Default Attribute Table size. */ +/** @} */ + +/** @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. */ +/** @} */ + +/** @} */ + +/** @addtogroup BLE_GATTS_STRUCTURES Structures + * @{ */ + +/** + * @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 */ +} 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_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_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 */ +} 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. */ +} 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_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. */ +} 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. */ +} 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. */ +} 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. */ +} 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. */ +} ble_gatts_cfg_service_changed_t; + +/**@brief Service Changed CCCD permission configuration parameters, set with @ref sd_ble_cfg_set. + * + * @note @ref ble_gatts_attr_md_t::vlen is ignored and should be set to 0. + * + * @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. + * - 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_cfg_service_changed_cccd_perm_t; + +/**@brief Attribute table size configuration parameters, set with @ref sd_ble_cfg_set. + * + * @retval ::NRF_ERROR_INVALID_LENGTH One or more of the following is true: + * - The specified Attribute Table size is too small. + * The minimum acceptable size is defined by @ref BLE_GATTS_ATTR_TAB_SIZE_MIN. + * - 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. */ +} 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_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. */ +} 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. */ +} 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. */ +} 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). */ +} ble_gatts_evt_sys_attr_missing_t; + +/**@brief Event structure for @ref BLE_GATTS_EVT_HVC. */ +typedef struct { + 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. */ +} 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. */ +} 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. */ +} 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. */ +} ble_gatts_evt_t; + +/** @} */ + +/** @addtogroup BLE_GATTS_FUNCTIONS Functions + * @{ */ + +/**@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. + * + * @mscs + * @mmsc{@ref BLE_GATTS_ATT_TABLE_POP_MSC} + * @endmscs + * + * @param[in] type Toggles between primary and secondary services, see @ref BLE_GATTS_SRVC_TYPES. + * @param[in] p_uuid Pointer to service UUID. + * @param[out] p_handle Pointer to a 16-bit word where the assigned handle will be stored. + * + * @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_FORBIDDEN Forbidden value supplied, certain UUIDs are reserved for the stack. + * @retval ::NRF_ERROR_NO_MEM Not enough memory to complete operation. + */ +SVCALL(SD_BLE_GATTS_SERVICE_ADD, uint32_t, sd_ble_gatts_service_add(uint8_t type, ble_uuid_t const *p_uuid, uint16_t *p_handle)); + +/**@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 The included service must already be present in the Attribute Table prior to this call. + * + * @mscs + * @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] 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_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)); + +/**@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 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. + * + * @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] 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. + * + * @retval ::NRF_SUCCESS Successfully added a characteristic. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied, service handle, Vendor Specific UUIDs, lengths, and + * permissions need to adhere to the constraints. + * @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. + */ +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)); + +/**@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). + * + * @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] 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_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. + */ +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. + * + * @note Values other than system attributes can be set at any time, regardless of whether any active connections exist. + * + * @mscs + * @mmsc{@ref BLE_GATTS_QUEUED_WRITE_QUEUE_FULL_MSC} + * @mmsc{@ref BLE_GATTS_QUEUED_WRITE_NOBUF_NOAUTH_MSC} + * @endmscs + * + * @param[in] conn_handle Connection handle. Ignored if the value does not belong to a system attribute. + * @param[in] handle Attribute handle. + * @param[in,out] p_value Attribute value information. + * + * @retval ::NRF_SUCCESS Successfully set the value of the attribute. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @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 ::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)); + +/**@brief Get the value of a given attribute. + * + * @note If the attribute value is longer than the size of the supplied buffer, + * @ref ble_gatts_value_t::len will return the total attribute value length (excluding offset), + * and not the number of bytes actually returned in @ref ble_gatts_value_t::p_value. + * The application may use this information to allocate a suitable buffer size. + * + * @note When retrieving system attribute values with this function, the connection handle + * may refer to an already disconnected connection. Refer to the documentation of + * @ref sd_ble_gatts_sys_attr_get for further information. + * + * @param[in] conn_handle Connection handle. Ignored if the value does not belong to a system attribute. + * @param[in] handle Attribute handle. + * @param[in,out] p_value Attribute value information. + * + * @retval ::NRF_SUCCESS Successfully retrieved the value of the attribute. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @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. + */ +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. + * + * @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 + * ble_gatts_hvx_params_t::p_len). + * + * @note Only one indication procedure can be ongoing per connection at a time. + * If the application tries to indicate an attribute value while another indication procedure is ongoing, + * the function call will return @ref NRF_ERROR_BUSY. + * 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. + * + * @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. + * + * @events + * @event{@ref BLE_GATTS_EVT_HVN_TX_COMPLETE, Notification transmission complete.} + * @event{@ref BLE_GATTS_EVT_HVC, Confirmation received from the peer.} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_GATTS_HVX_SYS_ATTRS_MISSING_MSC} + * @mmsc{@ref BLE_GATTS_HVN_MSC} + * @mmsc{@ref BLE_GATTS_HVI_MSC} + * @mmsc{@ref BLE_GATTS_HVX_DISABLED_MSC} + * @endmscs + * + * @param[in] conn_handle Connection handle. + * @param[in,out] p_hvx_params Pointer to an HVx parameters structure. If @ref ble_gatts_hvx_params_t::p_data + * contains a non-NULL pointer the attribute value will be updated with the contents + * pointed by it before sending the notification or indication. If the attribute value + * 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 ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. + * @retval ::NRF_ERROR_INVALID_STATE One or more of the following is true: + * - Invalid Connection State + * - Notifications and/or indications not enabled in the CCCD + * - 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 ::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_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_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 + * reestablishing the connection. + */ +SVCALL(SD_BLE_GATTS_HVX, uint32_t, sd_ble_gatts_hvx(uint16_t conn_handle, ble_gatts_hvx_params_t const *p_hvx_params)); + +/**@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. + * + * @note Some of the restrictions and limitations that apply to @ref sd_ble_gatts_hvx also apply here. + * + * @events + * @event{@ref BLE_GATTS_EVT_SC_CONFIRM, Confirmation of attribute table change received from peer.} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_GATTS_SC_MSC} + * @endmscs + * + * @param[in] conn_handle Connection handle. + * @param[in] start_handle Start of affected attribute handle range. + * @param[in] end_handle End of affected attribute handle range. + * + * @retval ::NRF_SUCCESS Successfully queued the Service Changed indication for transmission. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. + * @retval ::NRF_ERROR_NOT_SUPPORTED Service Changed not enabled at initialization. See @ref + * sd_ble_cfg_set and @ref ble_gatts_cfg_service_changed_t. + * @retval ::NRF_ERROR_INVALID_STATE One or more of the following is true: + * - Invalid Connection State + * - 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 ::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 ::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)); + +/**@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. + * + * @mscs + * @mmsc{@ref BLE_GATTS_QUEUED_WRITE_NOBUF_AUTH_MSC} + * @mmsc{@ref BLE_GATTS_QUEUED_WRITE_BUF_AUTH_MSC} + * @mmsc{@ref BLE_GATTS_QUEUED_WRITE_NOBUF_NOAUTH_MSC} + * @mmsc{@ref BLE_GATTS_READ_REQ_AUTH_MSC} + * @mmsc{@ref BLE_GATTS_WRITE_REQ_AUTH_MSC} + * @mmsc{@ref BLE_GATTS_QUEUED_WRITE_QUEUE_FULL_MSC} + * @mmsc{@ref BLE_GATTS_QUEUED_WRITE_PEER_CANCEL_MSC} + * @endmscs + * + * @param[in] conn_handle Connection handle. + * @param[in] p_rw_authorize_reply_params Pointer to a structure with the attribute provided by the application. + * + * @note @ref ble_gatts_authorize_params_t::p_data is ignored when this function is used to respond + * 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 ::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. + * @retval ::NRF_ERROR_INVALID_STATE Invalid Connection State or no authorization request pending. + * @retval ::NRF_ERROR_INVALID_PARAM Authorization op invalid, + * handle supplied does not match requested handle, + * or invalid data to be written provided by the application. + * @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_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)); + +/**@brief Update persistent system attribute information. + * + * @details Supply information about persistent system attributes to the stack, + * previously obtained using @ref sd_ble_gatts_sys_attr_get. + * This call is only allowed for active connections, and is usually + * made immediately after a connection is established with an known bonded device, + * often as a response to a @ref BLE_GATTS_EVT_SYS_ATTR_MISSING. + * + * p_sysattrs may point directly to the application's stored copy of the system attributes + * obtained using @ref sd_ble_gatts_sys_attr_get. + * 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 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. + * + * @mscs + * @mmsc{@ref BLE_GATTS_HVX_SYS_ATTRS_MISSING_MSC} + * @mmsc{@ref BLE_GATTS_SYS_ATTRS_UNK_PEER_MSC} + * @mmsc{@ref BLE_GATTS_SYS_ATTRS_BONDED_PEER_MSC} + * @endmscs + * + * @param[in] conn_handle Connection handle. + * @param[in] p_sys_attr_data Pointer to a saved copy of system attributes supplied to the stack, or NULL. + * @param[in] len Size of data pointed by p_sys_attr_data, in octets. + * @param[in] flags Optional additional flags, see @ref BLE_GATTS_SYS_ATTR_FLAGS + * + * @retval ::NRF_SUCCESS Successfully set the system attribute information. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_INVALID_STATE Invalid Connection State. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid flags supplied. + * @retval ::NRF_ERROR_INVALID_DATA Invalid data supplied, the data should be exactly the same as retrieved with @ref + * sd_ble_gatts_sys_attr_get. + * @retval ::NRF_ERROR_NO_MEM Not enough memory to complete operation. + */ +SVCALL(SD_BLE_GATTS_SYS_ATTR_SET, uint32_t, + sd_ble_gatts_sys_attr_set(uint16_t conn_handle, uint8_t const *p_sys_attr_data, uint16_t len, uint32_t flags)); + +/**@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. + * + * @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[in] flags Optional additional flags, see @ref BLE_GATTS_SYS_ATTR_FLAGS + * + * @retval ::NRF_SUCCESS Successfully retrieved the system attribute information. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid flags supplied. + * @retval ::NRF_ERROR_DATA_SIZE The system attribute information did not fit into the provided buffer. + * @retval ::NRF_ERROR_NOT_FOUND No system attributes found. + */ +SVCALL(SD_BLE_GATTS_SYS_ATTR_GET, uint32_t, + sd_ble_gatts_sys_attr_get(uint16_t conn_handle, uint8_t *p_sys_attr_data, uint16_t *p_len, uint32_t flags)); + +/**@brief Retrieve the first valid user attribute handle. + * + * @param[out] p_handle Pointer to an integer where the handle will be stored. + * + * @retval ::NRF_SUCCESS Successfully retrieved the handle. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + */ +SVCALL(SD_BLE_GATTS_INITIAL_USER_HANDLE_GET, uint32_t, sd_ble_gatts_initial_user_handle_get(uint16_t *p_handle)); + +/**@brief Retrieve the attribute UUID and/or metadata. + * + * @param[in] handle Attribute handle + * @param[out] p_uuid UUID of the attribute. Use NULL to omit this field. + * @param[out] p_md Metadata of the attribute. Use NULL to omit this field. + * + * @retval ::NRF_SUCCESS Successfully retrieved the attribute metadata, + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameters supplied. Returned when both @c p_uuid and @c p_md are NULL. + * @retval ::NRF_ERROR_NOT_FOUND Attribute was not found. + */ +SVCALL(SD_BLE_GATTS_ATTR_GET, uint32_t, sd_ble_gatts_attr_get(uint16_t handle, ble_uuid_t *p_uuid, ble_gatts_attr_md_t *p_md)); + +/**@brief Reply to an ATT_MTU exchange request by sending an Exchange MTU Response to the client. + * + * @details This function is only used to reply to a @ref BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST event. + * + * @details The SoftDevice sets ATT_MTU to the minimum of: + * - The Client RX MTU value from @ref BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST, and + * - The Server RX MTU value. + * + * However, the SoftDevice never sets ATT_MTU lower than @ref BLE_GATT_ATT_MTU_DEFAULT. + * + * @mscs + * @mmsc{@ref BLE_GATTS_MTU_EXCHANGE} + * @endmscs + * + * @param[in] conn_handle The connection handle identifying the connection to perform this procedure on. + * @param[in] server_rx_mtu Server RX MTU size. + * - 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. + * + * @retval ::NRF_SUCCESS Successfully sent response to the client. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. + * @retval ::NRF_ERROR_INVALID_STATE Invalid Connection State or no ATT_MTU exchange request pending. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid Server RX MTU size supplied. + * @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_EXCHANGE_MTU_REPLY, uint32_t, sd_ble_gatts_exchange_mtu_reply(uint16_t conn_handle, uint16_t server_rx_mtu)); +/** @} */ + +#ifdef __cplusplus +} +#endif +#endif // BLE_GATTS_H__ + +/** + @} +*/ diff --git a/src/platform/nrf52/softdevice/ble_hci.h b/src/platform/nrf52/softdevice/ble_hci.h new file mode 100644 index 0000000..27f85d5 --- /dev/null +++ b/src/platform/nrf52/softdevice/ble_hci.h @@ -0,0 +1,135 @@ +/* + * Copyright (c) Nordic Semiconductor ASA + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form, except as embedded into a Nordic + * Semiconductor ASA integrated circuit in a product or a software update for + * such product, must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of Nordic Semiconductor ASA nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * 4. This software, with or without modification, must only be used with a + * Nordic Semiconductor ASA integrated circuit. + * + * 5. Any software provided in binary form under this license must not be reverse + * engineered, decompiled, modified and/or disassembled. + * + * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + @addtogroup BLE_COMMON + @{ +*/ + +#ifndef BLE_HCI_H__ +#define BLE_HCI_H__ +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup BLE_HCI_STATUS_CODES Bluetooth status codes + * @{ */ + +#define BLE_HCI_STATUS_CODE_SUCCESS 0x00 /**< Success. */ +#define BLE_HCI_STATUS_CODE_UNKNOWN_BTLE_COMMAND 0x01 /**< Unknown BLE Command. */ +#define BLE_HCI_STATUS_CODE_UNKNOWN_CONNECTION_IDENTIFIER 0x02 /**< Unknown Connection Identifier. */ +/*0x03 Hardware Failure +0x04 Page Timeout +*/ +#define BLE_HCI_AUTHENTICATION_FAILURE 0x05 /**< Authentication Failure. */ +#define BLE_HCI_STATUS_CODE_PIN_OR_KEY_MISSING 0x06 /**< Pin or Key missing. */ +#define BLE_HCI_MEMORY_CAPACITY_EXCEEDED 0x07 /**< Memory Capacity Exceeded. */ +#define BLE_HCI_CONNECTION_TIMEOUT 0x08 /**< Connection Timeout. */ +/*0x09 Connection Limit Exceeded +0x0A Synchronous Connection Limit To A Device Exceeded +0x0B ACL Connection Already Exists*/ +#define BLE_HCI_STATUS_CODE_COMMAND_DISALLOWED 0x0C /**< Command Disallowed. */ +/*0x0D Connection Rejected due to Limited Resources +0x0E Connection Rejected Due To Security Reasons +0x0F Connection Rejected due to Unacceptable BD_ADDR +0x10 Connection Accept Timeout Exceeded +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 \ + 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. */ +/* +0x17 Repeated Attempts +0x18 Pairing Not Allowed +0x19 Unknown LMP PDU +*/ +#define BLE_HCI_UNSUPPORTED_REMOTE_FEATURE 0x1A /**< Unsupported Remote Feature. */ +/* +0x1B SCO Offset Rejected +0x1C SCO Interval Rejected +0x1D SCO Air Mode Rejected*/ +#define BLE_HCI_STATUS_CODE_INVALID_LMP_PARAMETERS 0x1E /**< Invalid LMP Parameters. */ +#define BLE_HCI_STATUS_CODE_UNSPECIFIED_ERROR 0x1F /**< Unspecified Error. */ +/*0x20 Unsupported LMP Parameter Value +0x21 Role Change Not Allowed +*/ +#define BLE_HCI_STATUS_CODE_LMP_RESPONSE_TIMEOUT 0x22 /**< LMP Response Timeout. */ +#define BLE_HCI_STATUS_CODE_LMP_ERROR_TRANSACTION_COLLISION 0x23 /**< LMP Error Transaction Collision/LL Procedure Collision. */ +#define BLE_HCI_STATUS_CODE_LMP_PDU_NOT_ALLOWED 0x24 /**< LMP PDU Not Allowed. */ +/*0x25 Encryption Mode Not Acceptable +0x26 Link Key Can Not be Changed +0x27 Requested QoS Not Supported +*/ +#define BLE_HCI_INSTANT_PASSED 0x28 /**< Instant Passed. */ +#define BLE_HCI_PAIRING_WITH_UNIT_KEY_UNSUPPORTED 0x29 /**< Pairing with Unit Key Unsupported. */ +#define BLE_HCI_DIFFERENT_TRANSACTION_COLLISION 0x2A /**< Different Transaction Collision. */ +/* +0x2B Reserved +0x2C QoS Unacceptable Parameter +0x2D QoS Rejected +0x2E Channel Classification Not Supported +0x2F Insufficient Security +*/ +#define BLE_HCI_PARAMETER_OUT_OF_MANDATORY_RANGE 0x30 /**< Parameter Out Of Mandatory Range. */ +/* +0x31 Reserved +0x32 Role Switch Pending +0x33 Reserved +0x34 Reserved Slot Violation +0x35 Role Switch Failed +0x36 Extended Inquiry Response Too Large +0x37 Secure Simple Pairing Not Supported By Host. +0x38 Host Busy - Pairing +0x39 Connection Rejected due to No Suitable Channel Found*/ +#define BLE_HCI_CONTROLLER_BUSY 0x3A /**< Controller Busy. */ +#define BLE_HCI_CONN_INTERVAL_UNACCEPTABLE 0x3B /**< Connection Interval Unacceptable. */ +#define BLE_HCI_DIRECTED_ADVERTISER_TIMEOUT 0x3C /**< Directed Advertisement Timeout. */ +#define BLE_HCI_CONN_TERMINATED_DUE_TO_MIC_FAILURE 0x3D /**< Connection Terminated due to MIC Failure. */ +#define BLE_HCI_CONN_FAILED_TO_BE_ESTABLISHED 0x3E /**< Connection Failed to be Established. */ + +/** @} */ + +#ifdef __cplusplus +} +#endif +#endif // BLE_HCI_H__ + +/** @} */ diff --git a/src/platform/nrf52/softdevice/ble_l2cap.h b/src/platform/nrf52/softdevice/ble_l2cap.h new file mode 100644 index 0000000..5f4bd27 --- /dev/null +++ b/src/platform/nrf52/softdevice/ble_l2cap.h @@ -0,0 +1,495 @@ +/* + * Copyright (c) Nordic Semiconductor ASA + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form, except as embedded into a Nordic + * Semiconductor ASA integrated circuit in a product or a software update for + * such product, must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of Nordic Semiconductor ASA nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * 4. This software, with or without modification, must only be used with a + * Nordic Semiconductor ASA integrated circuit. + * + * 5. Any software provided in binary form under this license must not be reverse + * engineered, decompiled, modified and/or disassembled. + * + * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + @addtogroup BLE_L2CAP Logical Link Control and Adaptation Protocol (L2CAP) + @{ + @brief Definitions and prototypes for the L2CAP interface. + */ + +#ifndef BLE_L2CAP_H__ +#define BLE_L2CAP_H__ + +#include "ble_err.h" +#include "ble_ranges.h" +#include "ble_types.h" +#include "nrf_error.h" +#include "nrf_svc.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/**@addtogroup BLE_L2CAP_TERMINOLOGY Terminology + * @{ + * @details + * + * L2CAP SDU + * - A data unit that the application can send/receive to/from a peer. + * + * L2CAP PDU + * - A data unit that is exchanged between local and remote L2CAP entities. + * It consists of L2CAP protocol control information and payload fields. + * The payload field can contain an L2CAP SDU or a part of an L2CAP SDU. + * + * L2CAP MTU + * - The maximum length of an L2CAP SDU. + * + * L2CAP MPS + * - The maximum length of an L2CAP PDU payload field. + * + * Credits + * - A value indicating the number of L2CAP PDUs that the receiver of the credit can send to the peer. + * @} */ + +/**@addtogroup BLE_L2CAP_ENUMERATIONS Enumerations + * @{ */ + +/**@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. */ +}; + +/**@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. */ +}; + +/** @} */ + +/**@addtogroup BLE_L2CAP_DEFINES Defines + * @{ */ + +/**@brief Maximum number of L2CAP channels per connection. */ +#define BLE_L2CAP_CH_COUNT_MAX (64) + +/**@brief Minimum L2CAP MTU, in bytes. */ +#define BLE_L2CAP_MTU_MIN (23) + +/**@brief Minimum L2CAP MPS, in bytes. */ +#define BLE_L2CAP_MPS_MIN (23) + +/**@brief Invalid CID. */ +#define BLE_L2CAP_CID_INVALID (0x0000) + +/**@brief Default number of credits for @ref sd_ble_l2cap_ch_flow_control. */ +#define BLE_L2CAP_CREDITS_DEFAULT (1) + +/**@defgroup BLE_L2CAP_CH_SETUP_REFUSED_SRCS L2CAP channel setup refused sources + * @{ */ +#define BLE_L2CAP_CH_SETUP_REFUSED_SRC_LOCAL (0x01) /**< Local. */ +#define BLE_L2CAP_CH_SETUP_REFUSED_SRC_REMOTE (0x02) /**< Remote. */ + /** @} */ + +/** @defgroup BLE_L2CAP_CH_STATUS_CODES L2CAP channel status codes + * @{ */ +#define BLE_L2CAP_CH_STATUS_CODE_SUCCESS (0x0000) /**< Success. */ +#define BLE_L2CAP_CH_STATUS_CODE_LE_PSM_NOT_SUPPORTED (0x0002) /**< LE_PSM not supported. */ +#define BLE_L2CAP_CH_STATUS_CODE_NO_RESOURCES (0x0004) /**< No resources available. */ +#define BLE_L2CAP_CH_STATUS_CODE_INSUFF_AUTHENTICATION (0x0005) /**< Insufficient authentication. */ +#define BLE_L2CAP_CH_STATUS_CODE_INSUFF_AUTHORIZATION (0x0006) /**< Insufficient authorization. */ +#define BLE_L2CAP_CH_STATUS_CODE_INSUFF_ENC_KEY_SIZE (0x0007) /**< Insufficient encryption key size. */ +#define BLE_L2CAP_CH_STATUS_CODE_INSUFF_ENC (0x0008) /**< Insufficient encryption. */ +#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. */ +/** @} */ + +/** @} */ + +/**@addtogroup BLE_L2CAP_STRUCTURES Structures + * @{ */ + +/** + * @brief BLE L2CAP connection configuration parameters, set with @ref sd_ble_cfg_set. + * + * @note These parameters are set per connection, so all L2CAP channels created on this connection + * will have the same parameters. + * + * @retval ::NRF_ERROR_INVALID_PARAM One or more of the following is true: + * - rx_mps is smaller than @ref BLE_L2CAP_MPS_MIN. + * - tx_mps is smaller than @ref BLE_L2CAP_MPS_MIN. + * - ch_count is greater than @ref BLE_L2CAP_CH_COUNT_MAX. + * @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. */ +} 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. */ +} 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_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. */ +} 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_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 */ +} 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_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_l2cap_evt_ch_sdu_buf_released_t; + +/**@brief L2CAP Channel Credit received event. */ +typedef struct { + 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. */ +} ble_l2cap_evt_ch_rx_t; + +/**@brief L2CAP Channel transmitted SDU event. */ +typedef struct { + 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. */ +} ble_l2cap_evt_t; + +/** @} */ + +/**@addtogroup BLE_L2CAP_FUNCTIONS Functions + * @{ */ + +/**@brief Set up an L2CAP channel. + * + * @details This function is used to: + * - Request setup of an L2CAP channel: sends an LE Credit Based Connection Request packet to a peer. + * - Reply to a setup request of an L2CAP channel (if called in response to a + * @ref BLE_L2CAP_EVT_CH_SETUP_REQUEST event): sends an LE Credit Based Connection + * Response packet to a peer. + * + * @note A call to this function will require the application to keep the SDU data buffer alive + * until the SDU data buffer is returned in @ref BLE_L2CAP_EVT_CH_RX or + * @ref BLE_L2CAP_EVT_CH_SDU_BUF_RELEASED event. + * + * @events + * @event{@ref BLE_L2CAP_EVT_CH_SETUP, Setup successful.} + * @event{@ref BLE_L2CAP_EVT_CH_SETUP_REFUSED, Setup failed.} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_L2CAP_CH_SETUP_MSC} + * @endmscs + * + * @param[in] conn_handle Connection Handle. + * @param[in,out] p_local_cid Pointer to a uint16_t containing Local Channel ID of the L2CAP channel: + * - As input: @ref BLE_L2CAP_CID_INVALID when requesting setup of an L2CAP + * channel or local_cid provided in the @ref BLE_L2CAP_EVT_CH_SETUP_REQUEST + * event when replying to a setup request of an L2CAP channel. + * - As output: local_cid for this channel. + * @param[in] p_params L2CAP channel parameters. + * + * @retval ::NRF_SUCCESS Successfully queued request or response for transmission. + * @retval ::NRF_ERROR_BUSY The stack is busy, process pending events and retry. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. + * @retval ::NRF_ERROR_INVALID_LENGTH Supplied higher rx_mps than has been configured on this link. + * @retval ::NRF_ERROR_INVALID_STATE Invalid State to perform operation (L2CAP channel already set up). + * @retval ::NRF_ERROR_NOT_FOUND CID not found. + * @retval ::NRF_ERROR_RESOURCES The limit has been reached for available L2CAP channels, + * see @ref ble_l2cap_conn_cfg_t::ch_count. + */ +SVCALL(SD_BLE_L2CAP_CH_SETUP, uint32_t, + sd_ble_l2cap_ch_setup(uint16_t conn_handle, uint16_t *p_local_cid, ble_l2cap_ch_setup_params_t const *p_params)); + +/**@brief Release an L2CAP channel. + * + * @details This sends a Disconnection Request packet to a peer. + * + * @events + * @event{@ref BLE_L2CAP_EVT_CH_RELEASED, Release complete.} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_L2CAP_CH_RELEASE_MSC} + * @endmscs + * + * @param[in] conn_handle Connection Handle. + * @param[in] local_cid Local Channel ID of the L2CAP channel. + * + * @retval ::NRF_SUCCESS Successfully queued request for transmission. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. + * @retval ::NRF_ERROR_INVALID_STATE Invalid State to perform operation (Setup or release is + * in progress for the L2CAP channel). + * @retval ::NRF_ERROR_NOT_FOUND CID not found. + */ +SVCALL(SD_BLE_L2CAP_CH_RELEASE, uint32_t, sd_ble_l2cap_ch_release(uint16_t conn_handle, uint16_t local_cid)); + +/**@brief Receive an SDU on an L2CAP channel. + * + * @details This may issue additional credits to the peer using an LE Flow Control Credit packet. + * + * @note A call to this function will require the application to keep the memory pointed by + * @ref ble_data_t::p_data alive until the SDU data buffer is returned in @ref BLE_L2CAP_EVT_CH_RX + * or @ref BLE_L2CAP_EVT_CH_SDU_BUF_RELEASED event. + * + * @note The SoftDevice can queue up to @ref ble_l2cap_conn_cfg_t::rx_queue_size SDU data buffers + * for reception per L2CAP channel. + * + * @events + * @event{@ref BLE_L2CAP_EVT_CH_RX, The SDU is received.} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_L2CAP_CH_RX_MSC} + * @endmscs + * + * @param[in] conn_handle Connection Handle. + * @param[in] local_cid Local Channel ID of the L2CAP channel. + * @param[in] p_sdu_buf Pointer to the SDU data buffer. + * + * @retval ::NRF_SUCCESS Buffer accepted. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. + * @retval ::NRF_ERROR_INVALID_STATE Invalid State to perform operation (Setup or release is + * in progress for an L2CAP channel). + * @retval ::NRF_ERROR_NOT_FOUND CID not found. + * @retval ::NRF_ERROR_RESOURCES Too many SDU data buffers supplied. Wait for a + * @ref BLE_L2CAP_EVT_CH_RX event and retry. + */ +SVCALL(SD_BLE_L2CAP_CH_RX, uint32_t, sd_ble_l2cap_ch_rx(uint16_t conn_handle, uint16_t local_cid, ble_data_t const *p_sdu_buf)); + +/**@brief Transmit an SDU on an L2CAP channel. + * + * @note A call to this function will require the application to keep the memory pointed by + * @ref ble_data_t::p_data alive until the SDU data buffer is returned in @ref BLE_L2CAP_EVT_CH_TX + * or @ref BLE_L2CAP_EVT_CH_SDU_BUF_RELEASED event. + * + * @note The SoftDevice can queue up to @ref ble_l2cap_conn_cfg_t::tx_queue_size SDUs for + * transmission per L2CAP channel. + * + * @note The application can keep track of the available credits for transmission by following + * the procedure below: + * - Store initial credits given by the peer in a variable. + * (Initial credits are provided in a @ref BLE_L2CAP_EVT_CH_SETUP event.) + * - Decrement the variable, which stores the currently available credits, by + * ceiling((@ref ble_data_t::len + 2) / tx_mps) when a call to this function returns + * @ref NRF_SUCCESS. (tx_mps is provided in a @ref BLE_L2CAP_EVT_CH_SETUP event.) + * - Increment the variable, which stores the currently available credits, by additional + * credits given by the peer in a @ref BLE_L2CAP_EVT_CH_CREDIT event. + * + * @events + * @event{@ref BLE_L2CAP_EVT_CH_TX, The SDU is transmitted.} + * @endevents + * + * @mscs + * @mmsc{@ref BLE_L2CAP_CH_TX_MSC} + * @endmscs + * + * @param[in] conn_handle Connection Handle. + * @param[in] local_cid Local Channel ID of the L2CAP channel. + * @param[in] p_sdu_buf Pointer to the SDU data buffer. + * + * @retval ::NRF_SUCCESS Successfully queued L2CAP SDU for transmission. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. + * @retval ::NRF_ERROR_INVALID_STATE Invalid State to perform operation (Setup or release is + * in progress for the L2CAP channel). + * @retval ::NRF_ERROR_NOT_FOUND CID not found. + * @retval ::NRF_ERROR_DATA_SIZE Invalid SDU length supplied, must not be more than + * @ref ble_l2cap_ch_tx_params_t::tx_mtu provided in + * @ref BLE_L2CAP_EVT_CH_SETUP event. + * @retval ::NRF_ERROR_RESOURCES Too many SDUs queued for transmission. Wait for a + * @ref BLE_L2CAP_EVT_CH_TX event and retry. + */ +SVCALL(SD_BLE_L2CAP_CH_TX, uint32_t, sd_ble_l2cap_ch_tx(uint16_t conn_handle, uint16_t local_cid, ble_data_t const *p_sdu_buf)); + +/**@brief Advanced SDU reception flow control. + * + * @details Adjust the way the SoftDevice issues credits to the peer. + * This may issue additional credits to the peer using an LE Flow Control Credit packet. + * + * @mscs + * @mmsc{@ref BLE_L2CAP_CH_FLOW_CONTROL_MSC} + * @endmscs + * + * @param[in] conn_handle Connection Handle. + * @param[in] local_cid Local Channel ID of the L2CAP channel or @ref BLE_L2CAP_CID_INVALID to set + * the value that will be used for newly created channels. + * @param[in] credits Number of credits that the SoftDevice will make sure the peer has every + * time it starts using a new reception buffer. + * - @ref BLE_L2CAP_CREDITS_DEFAULT is the default value the SoftDevice will + * use if this function is not called. + * - If set to zero, the SoftDevice will stop issuing credits for new reception + * buffers the application provides or has provided. SDU reception that is + * currently ongoing will be allowed to complete. + * @param[out] p_credits NULL or pointer to a uint16_t. If a valid pointer is provided, it will be + * written by the SoftDevice with the number of credits that is or will be + * available to the peer. If the value written by the SoftDevice is 0 when + * credits parameter was set to 0, the peer will not be able to send more + * data until more credits are provided by calling this function again with + * credits > 0. This parameter is ignored when local_cid is set to + * @ref BLE_L2CAP_CID_INVALID. + * + * @note Application should take care when setting number of credits higher than default value. In + * this case the application must make sure that the SoftDevice always has reception buffers + * available (see @ref sd_ble_l2cap_ch_rx) for that channel. If the SoftDevice does not have + * such buffers available, packets may be NACKed on the Link Layer and all Bluetooth traffic + * on the connection handle may be stalled until the SoftDevice again has an available + * reception buffer. This applies even if the application has used this call to set the + * credits back to default, or zero. + * + * @retval ::NRF_SUCCESS Flow control parameters accepted. + * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. + * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. + * @retval ::NRF_ERROR_INVALID_STATE Invalid State to perform operation (Setup or release is + * in progress for an L2CAP channel). + * @retval ::NRF_ERROR_NOT_FOUND CID not found. + */ +SVCALL(SD_BLE_L2CAP_CH_FLOW_CONTROL, uint32_t, + sd_ble_l2cap_ch_flow_control(uint16_t conn_handle, uint16_t local_cid, uint16_t credits, uint16_t *p_credits)); + +/** @} */ + +#ifdef __cplusplus +} +#endif +#endif // BLE_L2CAP_H__ + +/** + @} +*/ diff --git a/src/platform/nrf52/softdevice/ble_ranges.h b/src/platform/nrf52/softdevice/ble_ranges.h new file mode 100644 index 0000000..2768e49 --- /dev/null +++ b/src/platform/nrf52/softdevice/ble_ranges.h @@ -0,0 +1,149 @@ +/* + * Copyright (c) Nordic Semiconductor ASA + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form, except as embedded into a Nordic + * Semiconductor ASA integrated circuit in a product or a software update for + * such product, must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of Nordic Semiconductor ASA nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * 4. This software, with or without modification, must only be used with a + * Nordic Semiconductor ASA integrated circuit. + * + * 5. Any software provided in binary form under this license must not be reverse + * engineered, decompiled, modified and/or disassembled. + * + * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + @addtogroup BLE_COMMON + @{ + @defgroup ble_ranges Module specific SVC, event and option number subranges + @{ + + @brief Definition of SVC, event and option number subranges for each API module. + + @note + SVCs, event and option numbers are split into subranges for each API module. + Each module receives its entire allocated range of SVC calls, whether implemented or not, + but return BLE_ERROR_NOT_SUPPORTED for unimplemented or undefined calls in its range. + + Note that the symbols BLE__SVC_LAST is the end of the allocated SVC range, + rather than the last SVC function call actually defined and implemented. + + Specific SVC, event and option values are defined in each module's ble_.h file, + which defines names of each individual SVC code based on the range start value. +*/ + +#ifndef BLE_RANGES_H__ +#define BLE_RANGES_H__ + +#ifdef __cplusplus +extern "C" { +#endif + +#define BLE_SVC_BASE 0x60 /**< Common BLE SVC base. */ +#define BLE_SVC_LAST 0x6B /**< Common BLE SVC last. */ + +#define BLE_GAP_SVC_BASE 0x6C /**< GAP BLE SVC base. */ +#define BLE_GAP_SVC_LAST 0x9A /**< GAP BLE SVC last. */ + +#define BLE_GATTC_SVC_BASE 0x9B /**< GATTC BLE SVC base. */ +#define BLE_GATTC_SVC_LAST 0xA7 /**< GATTC BLE SVC last. */ + +#define BLE_GATTS_SVC_BASE 0xA8 /**< GATTS BLE SVC base. */ +#define BLE_GATTS_SVC_LAST 0xB7 /**< GATTS BLE SVC last. */ + +#define BLE_L2CAP_SVC_BASE 0xB8 /**< L2CAP BLE SVC base. */ +#define BLE_L2CAP_SVC_LAST 0xBF /**< L2CAP BLE SVC last. */ + +#define BLE_EVT_INVALID 0x00 /**< Invalid BLE Event. */ + +#define BLE_EVT_BASE 0x01 /**< Common BLE Event base. */ +#define BLE_EVT_LAST 0x0F /**< Common BLE Event last. */ + +#define BLE_GAP_EVT_BASE 0x10 /**< GAP BLE Event base. */ +#define BLE_GAP_EVT_LAST 0x2F /**< GAP BLE Event last. */ + +#define BLE_GATTC_EVT_BASE 0x30 /**< GATTC BLE Event base. */ +#define BLE_GATTC_EVT_LAST 0x4F /**< GATTC BLE Event last. */ + +#define BLE_GATTS_EVT_BASE 0x50 /**< GATTS BLE Event base. */ +#define BLE_GATTS_EVT_LAST 0x6F /**< GATTS BLE Event last. */ + +#define BLE_L2CAP_EVT_BASE 0x70 /**< L2CAP BLE Event base. */ +#define BLE_L2CAP_EVT_LAST 0x8F /**< L2CAP BLE Event last. */ + +#define BLE_OPT_INVALID 0x00 /**< Invalid BLE Option. */ + +#define BLE_OPT_BASE 0x01 /**< Common BLE Option base. */ +#define BLE_OPT_LAST 0x1F /**< Common BLE Option last. */ + +#define BLE_GAP_OPT_BASE 0x20 /**< GAP BLE Option base. */ +#define BLE_GAP_OPT_LAST 0x3F /**< GAP BLE Option last. */ + +#define BLE_GATT_OPT_BASE 0x40 /**< GATT BLE Option base. */ +#define BLE_GATT_OPT_LAST 0x5F /**< GATT BLE Option last. */ + +#define BLE_GATTC_OPT_BASE 0x60 /**< GATTC BLE Option base. */ +#define BLE_GATTC_OPT_LAST 0x7F /**< GATTC BLE Option last. */ + +#define BLE_GATTS_OPT_BASE 0x80 /**< GATTS BLE Option base. */ +#define BLE_GATTS_OPT_LAST 0x9F /**< GATTS BLE Option last. */ + +#define BLE_L2CAP_OPT_BASE 0xA0 /**< L2CAP BLE Option base. */ +#define BLE_L2CAP_OPT_LAST 0xBF /**< L2CAP BLE Option last. */ + +#define BLE_CFG_INVALID 0x00 /**< Invalid BLE configuration. */ + +#define BLE_CFG_BASE 0x01 /**< Common BLE configuration base. */ +#define BLE_CFG_LAST 0x1F /**< Common BLE configuration last. */ + +#define BLE_CONN_CFG_BASE 0x20 /**< BLE connection configuration base. */ +#define BLE_CONN_CFG_LAST 0x3F /**< BLE connection configuration last. */ + +#define BLE_GAP_CFG_BASE 0x40 /**< GAP BLE configuration base. */ +#define BLE_GAP_CFG_LAST 0x5F /**< GAP BLE configuration last. */ + +#define BLE_GATT_CFG_BASE 0x60 /**< GATT BLE configuration base. */ +#define BLE_GATT_CFG_LAST 0x7F /**< GATT BLE configuration last. */ + +#define BLE_GATTC_CFG_BASE 0x80 /**< GATTC BLE configuration base. */ +#define BLE_GATTC_CFG_LAST 0x9F /**< GATTC BLE configuration last. */ + +#define BLE_GATTS_CFG_BASE 0xA0 /**< GATTS BLE configuration base. */ +#define BLE_GATTS_CFG_LAST 0xBF /**< GATTS BLE configuration last. */ + +#define BLE_L2CAP_CFG_BASE 0xC0 /**< L2CAP BLE configuration base. */ +#define BLE_L2CAP_CFG_LAST 0xDF /**< L2CAP BLE configuration last. */ + +#ifdef __cplusplus +} +#endif +#endif /* BLE_RANGES_H__ */ + +/** + @} + @} +*/ diff --git a/src/platform/nrf52/softdevice/ble_types.h b/src/platform/nrf52/softdevice/ble_types.h new file mode 100644 index 0000000..db3656c --- /dev/null +++ b/src/platform/nrf52/softdevice/ble_types.h @@ -0,0 +1,217 @@ +/* + * Copyright (c) Nordic Semiconductor ASA + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form, except as embedded into a Nordic + * Semiconductor ASA integrated circuit in a product or a software update for + * such product, must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of Nordic Semiconductor ASA nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * 4. This software, with or without modification, must only be used with a + * Nordic Semiconductor ASA integrated circuit. + * + * 5. Any software provided in binary form under this license must not be reverse + * engineered, decompiled, modified and/or disassembled. + * + * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + @addtogroup BLE_COMMON + @{ + @defgroup ble_types Common types and macro definitions + @{ + + @brief Common types and macro definitions for the BLE SoftDevice. + */ + +#ifndef BLE_TYPES_H__ +#define BLE_TYPES_H__ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @addtogroup BLE_TYPES_DEFINES Defines + * @{ */ + +/** @defgroup BLE_CONN_HANDLES BLE Connection Handles + * @{ */ +#define BLE_CONN_HANDLE_INVALID 0xFFFF /**< Invalid Connection Handle. */ +#define BLE_CONN_HANDLE_ALL 0xFFFE /**< Applies to all Connection Handles. */ +/** @} */ + +/** @defgroup BLE_UUID_VALUES Assigned Values for BLE UUIDs + * @{ */ +/* Generic UUIDs, applicable to all services */ +#define BLE_UUID_UNKNOWN 0x0000 /**< Reserved UUID. */ +#define BLE_UUID_SERVICE_PRIMARY 0x2800 /**< Primary Service. */ +#define BLE_UUID_SERVICE_SECONDARY 0x2801 /**< Secondary Service. */ +#define BLE_UUID_SERVICE_INCLUDE 0x2802 /**< Include. */ +#define BLE_UUID_CHARACTERISTIC 0x2803 /**< Characteristic. */ +#define BLE_UUID_DESCRIPTOR_CHAR_EXT_PROP 0x2900 /**< Characteristic Extended Properties Descriptor. */ +#define BLE_UUID_DESCRIPTOR_CHAR_USER_DESC 0x2901 /**< Characteristic User Description Descriptor. */ +#define BLE_UUID_DESCRIPTOR_CLIENT_CHAR_CONFIG 0x2902 /**< Client Characteristic Configuration Descriptor. */ +#define BLE_UUID_DESCRIPTOR_SERVER_CHAR_CONFIG 0x2903 /**< Server Characteristic Configuration Descriptor. */ +#define BLE_UUID_DESCRIPTOR_CHAR_PRESENTATION_FORMAT 0x2904 /**< Characteristic Presentation Format Descriptor. */ +#define BLE_UUID_DESCRIPTOR_CHAR_AGGREGATE_FORMAT 0x2905 /**< Characteristic Aggregate Format Descriptor. */ +/* GATT specific UUIDs */ +#define BLE_UUID_GATT 0x1801 /**< Generic Attribute Profile. */ +#define BLE_UUID_GATT_CHARACTERISTIC_SERVICE_CHANGED 0x2A05 /**< Service Changed Characteristic. */ +/* GAP specific UUIDs */ +#define BLE_UUID_GAP 0x1800 /**< Generic Access Profile. */ +#define BLE_UUID_GAP_CHARACTERISTIC_DEVICE_NAME 0x2A00 /**< Device Name Characteristic. */ +#define BLE_UUID_GAP_CHARACTERISTIC_APPEARANCE 0x2A01 /**< Appearance Characteristic. */ +#define BLE_UUID_GAP_CHARACTERISTIC_RECONN_ADDR 0x2A03 /**< Reconnection Address Characteristic. */ +#define BLE_UUID_GAP_CHARACTERISTIC_PPCP 0x2A04 /**< Peripheral Preferred Connection Parameters Characteristic. */ +#define BLE_UUID_GAP_CHARACTERISTIC_CAR 0x2AA6 /**< Central Address Resolution Characteristic. */ +#define BLE_UUID_GAP_CHARACTERISTIC_RPA_ONLY 0x2AC9 /**< Resolvable Private Address Only Characteristic. */ +/** @} */ + +/** @defgroup BLE_UUID_TYPES Types of UUID + * @{ */ +#define BLE_UUID_TYPE_UNKNOWN 0x00 /**< Invalid UUID type. */ +#define BLE_UUID_TYPE_BLE 0x01 /**< Bluetooth SIG UUID (16-bit). */ +#define BLE_UUID_TYPE_VENDOR_BEGIN 0x02 /**< Vendor UUID types start at this index (128-bit). */ +/** @} */ + +/** @defgroup BLE_APPEARANCES Bluetooth Appearance values + * @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). */ +/** @} */ + +/** @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) + +/** @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) + +/** @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) + +/** @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)) + +/** @brief Compare for difference both type and uuid members of two (valid, non-null) ble_uuid_t pointers. */ +#define BLE_UUID_NEQ(p_uuid1, p_uuid2) (((p_uuid1)->type != (p_uuid2)->type) || ((p_uuid1)->uuid != (p_uuid2)->uuid)) + +/** @} */ + +/** @addtogroup BLE_TYPES_STRUCTURES Structures + * @{ */ + +/** @brief 128 bit UUID values. */ +typedef struct { + 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. */ +} 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. */ +} ble_data_t; + +/** @} */ +#ifdef __cplusplus +} +#endif + +#endif /* BLE_TYPES_H__ */ + +/** + @} + @} +*/ diff --git a/src/platform/nrf52/softdevice/nrf52/nrf_mbr.h b/src/platform/nrf52/softdevice/nrf52/nrf_mbr.h new file mode 100644 index 0000000..4e0bd75 --- /dev/null +++ b/src/platform/nrf52/softdevice/nrf52/nrf_mbr.h @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2014 - 2017, Nordic Semiconductor ASA + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form, except as embedded into a Nordic + * Semiconductor ASA integrated circuit in a product or a software update for + * such product, must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of Nordic Semiconductor ASA nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * 4. This software, with or without modification, must only be used with a + * Nordic Semiconductor ASA integrated circuit. + * + * 5. Any software provided in binary form under this license must not be reverse + * engineered, decompiled, modified and/or disassembled. + * + * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + @defgroup nrf_mbr_api Master Boot Record API + @{ + + @brief APIs for updating SoftDevice and BootLoader + +*/ + +#ifndef NRF_MBR_H__ +#define NRF_MBR_H__ + +#include "nrf_svc.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @addtogroup NRF_MBR_DEFINES Defines + * @{ */ + +/**@brief MBR SVC Base number. */ +#define MBR_SVC_BASE (0x18) + +/**@brief Page size in words. */ +#define MBR_PAGE_SIZE_IN_WORDS (1024) + +/** @brief The size that must be reserved for the MBR when a SoftDevice is written to flash. +This is the offset where the first byte of the SoftDevice hex file is written. */ +#define MBR_SIZE (0x1000) + +/** @brief Location (in the flash memory) of the bootloader address. */ +#define MBR_BOOTLOADER_ADDR (0xFF8) + +/** @brief Location (in UICR) of the bootloader address. */ +#define MBR_UICR_BOOTLOADER_ADDR (&(NRF_UICR->NRFFW[0])) + +/** @brief Location (in the flash memory) of the address of the MBR parameter page. */ +#define MBR_PARAM_PAGE_ADDR (0xFFC) + +/** @brief Location (in UICR) of the address of the MBR parameter page. */ +#define MBR_UICR_PARAM_PAGE_ADDR (&(NRF_UICR->NRFFW[1])) + +/** @} */ + +/** @addtogroup NRF_MBR_ENUMS Enumerations + * @{ */ + +/**@brief nRF Master Boot Record API SVC numbers. */ +enum NRF_MBR_SVCS { + 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*/ +}; + +/** @} */ + +/** @addtogroup NRF_MBR_TYPES Types + * @{ */ + +/**@brief This command copies part of a new SoftDevice + * + * The destination area is erased before copying. + * If dst is in the middle of a flash page, that whole flash page will be erased. + * If (dst+len) is in the middle of a flash page, that whole flash page will be erased. + * + * 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. + */ +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.*/ +} sd_mbr_command_copy_sd_t; + +/**@brief This command works like memcmp, but takes the length in words. + * + * @retval ::NRF_SUCCESS indicates that the contents of both memory blocks are equal. + * @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.*/ +} sd_mbr_command_compare_t; + +/**@brief This command copies a new BootLoader. + * + * The MBR assumes that either @ref MBR_BOOTLOADER_ADDR or @ref MBR_UICR_BOOTLOADER_ADDR is set to + * the address where the bootloader will be copied. If both addresses are set, the MBR will prioritize + * @ref MBR_BOOTLOADER_ADDR. + * + * The bootloader destination is erased by this function. + * If (destination+bl_len) is in the middle of a flash page, that whole flash page will be erased. + * + * This command requires that @ref MBR_PARAM_PAGE_ADDR or @ref MBR_UICR_PARAM_PAGE_ADDR is set, + * see @ref sd_mbr_command. + * + * This command will use the flash protect peripheral (BPROT or ACL) to protect the flash that is + * not intended to be written. + * + * On success, this function will not return. It will start the new bootloader from reset-vector as normal. + * + * @retval ::NRF_ERROR_INTERNAL indicates an internal error that should not happen. + * @retval ::NRF_ERROR_FORBIDDEN if the bootloader address is not set. + * @retval ::NRF_ERROR_INVALID_LENGTH if parameters attempts to read or write outside flash area. + * @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. */ +} sd_mbr_command_copy_bl_t; + +/**@brief Change the address the MBR starts after a reset + * + * Once this function has been called, this address is where the MBR will start to forward + * interrupts to after a reset. + * + * To restore default forwarding, this function should be called with @ref address set to 0. If a + * bootloader is present, interrupts will be forwarded to the bootloader. If not, interrupts will + * be forwarded to the SoftDevice. + * + * The location of a bootloader can be specified in @ref MBR_BOOTLOADER_ADDR or + * @ref MBR_UICR_BOOTLOADER_ADDR. If both addresses are set, the MBR will prioritize + * @ref MBR_BOOTLOADER_ADDR. + * + * This command requires that @ref MBR_PARAM_PAGE_ADDR or @ref MBR_UICR_PARAM_PAGE_ADDR is set, + * see @ref sd_mbr_command. + * + * On success, this function will not return. It will reset the device. + * + * @retval ::NRF_ERROR_INTERNAL indicates an internal error that should not happen. + * @retval ::NRF_ERROR_INVALID_ADDR if parameter address is outside of the flash size. + * @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.*/ +} sd_mbr_command_vector_table_base_set_t; + +/**@brief Sets the base address of the interrupt vector table for interrupts forwarded from the MBR + * + * Unlike sd_mbr_command_vector_table_base_set_t, this function does not reset, and it does not + * change where the MBR starts after reset. + * + * @retval ::NRF_SUCCESS + */ +typedef struct { + 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 + * + * Depending on what command value that is set, the corresponding params value type must also be + * set. See @ref NRF_MBR_COMMANDS for command types and corresponding params value type. If command + * @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. */ +} sd_mbr_command_t; + +/** @} */ + +/** @addtogroup NRF_MBR_FUNCTIONS Functions + * @{ */ + +/**@brief Issue Master Boot Record commands + * + * Commands used when updating a SoftDevice and bootloader. + * + * The @ref SD_MBR_COMMAND_COPY_BL and @ref SD_MBR_COMMAND_VECTOR_TABLE_BASE_SET requires + * parameters to be retained by the MBR when resetting the IC. This is done in a separate flash + * page. The location of the flash page should be provided by the application in either + * @ref MBR_PARAM_PAGE_ADDR or @ref MBR_UICR_PARAM_PAGE_ADDR. If both addresses are set, the MBR + * will prioritize @ref MBR_PARAM_PAGE_ADDR. This page will be cleared by the MBR and is used to + * store the command before reset. When an address is specified, the page it refers to must not be + * used by the application. If no address is provided by the application, i.e. both + * @ref MBR_PARAM_PAGE_ADDR and @ref MBR_UICR_PARAM_PAGE_ADDR is 0xFFFFFFFF, MBR commands which use + * flash will be unavailable and return @ref NRF_ERROR_NO_MEM. + * + * @param[in] param Pointer to a struct describing the command. + * + * @note For a complete set of return values, see ::sd_mbr_command_copy_sd_t, + * ::sd_mbr_command_copy_bl_t, ::sd_mbr_command_compare_t, + * ::sd_mbr_command_vector_table_base_set_t, ::sd_mbr_command_irq_forward_address_set_t + * + * @retval ::NRF_ERROR_NO_MEM No MBR parameter page provided + * @retval ::NRF_ERROR_INVALID_PARAM if an invalid command is given. + */ +SVCALL(SD_MBR_COMMAND, uint32_t, sd_mbr_command(sd_mbr_command_t *param)); + +/** @} */ + +#ifdef __cplusplus +} +#endif +#endif // NRF_MBR_H__ + +/** + @} +*/ diff --git a/src/platform/nrf52/softdevice/nrf_error.h b/src/platform/nrf52/softdevice/nrf_error.h new file mode 100644 index 0000000..fb2831e --- /dev/null +++ b/src/platform/nrf52/softdevice/nrf_error.h @@ -0,0 +1,90 @@ +/* + * Copyright (c) Nordic Semiconductor ASA + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form, except as embedded into a Nordic + * Semiconductor ASA integrated circuit in a product or a software update for + * such product, must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of Nordic Semiconductor ASA nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * 4. This software, with or without modification, must only be used with a + * Nordic Semiconductor ASA integrated circuit. + * + * 5. Any software provided in binary form under this license must not be reverse + * engineered, decompiled, modified and/or disassembled. + * + * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + @defgroup nrf_error SoftDevice Global Error Codes + @{ + + @brief Global Error definitions +*/ + +/* Header guard */ +#ifndef NRF_ERROR_H__ +#define NRF_ERROR_H__ + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup NRF_ERRORS_BASE Error Codes Base number definitions + * @{ */ +#define NRF_ERROR_BASE_NUM (0x0) ///< Global error base +#define NRF_ERROR_SDM_BASE_NUM (0x1000) ///< SDM error base +#define NRF_ERROR_SOC_BASE_NUM (0x2000) ///< SoC error base +#define NRF_ERROR_STK_BASE_NUM (0x3000) ///< STK error base +/** @} */ + +#define NRF_SUCCESS (NRF_ERROR_BASE_NUM + 0) ///< Successful command +#define NRF_ERROR_SVC_HANDLER_MISSING (NRF_ERROR_BASE_NUM + 1) ///< SVC handler is missing +#define NRF_ERROR_SOFTDEVICE_NOT_ENABLED (NRF_ERROR_BASE_NUM + 2) ///< SoftDevice has not been enabled +#define NRF_ERROR_INTERNAL (NRF_ERROR_BASE_NUM + 3) ///< Internal Error +#define NRF_ERROR_NO_MEM (NRF_ERROR_BASE_NUM + 4) ///< No Memory for operation +#define NRF_ERROR_NOT_FOUND (NRF_ERROR_BASE_NUM + 5) ///< Not found +#define NRF_ERROR_NOT_SUPPORTED (NRF_ERROR_BASE_NUM + 6) ///< Not supported +#define NRF_ERROR_INVALID_PARAM (NRF_ERROR_BASE_NUM + 7) ///< Invalid Parameter +#define NRF_ERROR_INVALID_STATE (NRF_ERROR_BASE_NUM + 8) ///< Invalid state, operation disallowed in this state +#define NRF_ERROR_INVALID_LENGTH (NRF_ERROR_BASE_NUM + 9) ///< Invalid Length +#define NRF_ERROR_INVALID_FLAGS (NRF_ERROR_BASE_NUM + 10) ///< Invalid Flags +#define NRF_ERROR_INVALID_DATA (NRF_ERROR_BASE_NUM + 11) ///< Invalid Data +#define NRF_ERROR_DATA_SIZE (NRF_ERROR_BASE_NUM + 12) ///< Invalid Data size +#define NRF_ERROR_TIMEOUT (NRF_ERROR_BASE_NUM + 13) ///< Operation timed out +#define NRF_ERROR_NULL (NRF_ERROR_BASE_NUM + 14) ///< Null Pointer +#define NRF_ERROR_FORBIDDEN (NRF_ERROR_BASE_NUM + 15) ///< Forbidden Operation +#define NRF_ERROR_INVALID_ADDR (NRF_ERROR_BASE_NUM + 16) ///< Bad Memory Address +#define NRF_ERROR_BUSY (NRF_ERROR_BASE_NUM + 17) ///< Busy +#define NRF_ERROR_CONN_COUNT (NRF_ERROR_BASE_NUM + 18) ///< Maximum connection count exceeded. +#define NRF_ERROR_RESOURCES (NRF_ERROR_BASE_NUM + 19) ///< Not enough resources for operation + +#ifdef __cplusplus +} +#endif +#endif // NRF_ERROR_H__ + +/** + @} +*/ diff --git a/src/platform/nrf52/softdevice/nrf_error_sdm.h b/src/platform/nrf52/softdevice/nrf_error_sdm.h new file mode 100644 index 0000000..2fd6210 --- /dev/null +++ b/src/platform/nrf52/softdevice/nrf_error_sdm.h @@ -0,0 +1,73 @@ +/* + * Copyright (c) Nordic Semiconductor ASA + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form, except as embedded into a Nordic + * Semiconductor ASA integrated circuit in a product or a software update for + * such product, must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of Nordic Semiconductor ASA nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * 4. This software, with or without modification, must only be used with a + * Nordic Semiconductor ASA integrated circuit. + * + * 5. Any software provided in binary form under this license must not be reverse + * engineered, decompiled, modified and/or disassembled. + * + * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + @addtogroup nrf_sdm_api + @{ + @defgroup nrf_sdm_error SoftDevice Manager Error Codes + @{ + + @brief Error definitions for the SDM API +*/ + +/* Header guard */ +#ifndef NRF_ERROR_SDM_H__ +#define NRF_ERROR_SDM_H__ + +#include "nrf_error.h" + +#ifdef __cplusplus +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). + +#ifdef __cplusplus +} +#endif +#endif // NRF_ERROR_SDM_H__ + +/** + @} + @} +*/ diff --git a/src/platform/nrf52/softdevice/nrf_error_soc.h b/src/platform/nrf52/softdevice/nrf_error_soc.h new file mode 100644 index 0000000..cbd0ba8 --- /dev/null +++ b/src/platform/nrf52/softdevice/nrf_error_soc.h @@ -0,0 +1,85 @@ +/* + * Copyright (c) Nordic Semiconductor ASA + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form, except as embedded into a Nordic + * Semiconductor ASA integrated circuit in a product or a software update for + * such product, must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of Nordic Semiconductor ASA nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * 4. This software, with or without modification, must only be used with a + * Nordic Semiconductor ASA integrated circuit. + * + * 5. Any software provided in binary form under this license must not be reverse + * engineered, decompiled, modified and/or disassembled. + * + * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + @addtogroup nrf_soc_api + @{ + @defgroup nrf_soc_error SoC Library Error Codes + @{ + + @brief Error definitions for the SoC library + +*/ + +/* Header guard */ +#ifndef NRF_ERROR_SOC_H__ +#define NRF_ERROR_SOC_H__ + +#include "nrf_error.h" +#ifdef __cplusplus +extern "C" { +#endif + +/* Mutex Errors */ +#define NRF_ERROR_SOC_MUTEX_ALREADY_TAKEN (NRF_ERROR_SOC_BASE_NUM + 0) ///< Mutex already taken + +/* NVIC errors */ +#define NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE (NRF_ERROR_SOC_BASE_NUM + 1) ///< NVIC interrupt not available +#define NRF_ERROR_SOC_NVIC_INTERRUPT_PRIORITY_NOT_ALLOWED (NRF_ERROR_SOC_BASE_NUM + 2) ///< NVIC interrupt priority not allowed +#define NRF_ERROR_SOC_NVIC_SHOULD_NOT_RETURN (NRF_ERROR_SOC_BASE_NUM + 3) ///< NVIC should not return + +/* Power errors */ +#define NRF_ERROR_SOC_POWER_MODE_UNKNOWN (NRF_ERROR_SOC_BASE_NUM + 4) ///< Power mode unknown +#define NRF_ERROR_SOC_POWER_POF_THRESHOLD_UNKNOWN (NRF_ERROR_SOC_BASE_NUM + 5) ///< Power POF threshold unknown +#define NRF_ERROR_SOC_POWER_OFF_SHOULD_NOT_RETURN (NRF_ERROR_SOC_BASE_NUM + 6) ///< Power off should not return + +/* Rand errors */ +#define NRF_ERROR_SOC_RAND_NOT_ENOUGH_VALUES (NRF_ERROR_SOC_BASE_NUM + 7) ///< RAND not enough values + +/* PPI errors */ +#define NRF_ERROR_SOC_PPI_INVALID_CHANNEL (NRF_ERROR_SOC_BASE_NUM + 8) ///< Invalid PPI Channel +#define NRF_ERROR_SOC_PPI_INVALID_GROUP (NRF_ERROR_SOC_BASE_NUM + 9) ///< Invalid PPI Group + +#ifdef __cplusplus +} +#endif +#endif // NRF_ERROR_SOC_H__ +/** + @} + @} +*/ diff --git a/src/platform/nrf52/softdevice/nrf_nvic.h b/src/platform/nrf52/softdevice/nrf_nvic.h new file mode 100644 index 0000000..d4ab204 --- /dev/null +++ b/src/platform/nrf52/softdevice/nrf_nvic.h @@ -0,0 +1,449 @@ +/* + * Copyright (c) Nordic Semiconductor ASA + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form, except as embedded into a Nordic + * Semiconductor ASA integrated circuit in a product or a software update for + * such product, must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of Nordic Semiconductor ASA nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * 4. This software, with or without modification, must only be used with a + * Nordic Semiconductor ASA integrated circuit. + * + * 5. Any software provided in binary form under this license must not be reverse + * engineered, decompiled, modified and/or disassembled. + * + * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @defgroup nrf_nvic_api SoftDevice NVIC API + * @{ + * + * @note In order to use this module, the following code has to be added to a .c file: + * \code + * nrf_nvic_state_t nrf_nvic_state = {0}; + * \endcode + * + * @note Definitions and declarations starting with __ (double underscore) in this header file are + * not intended for direct use by the application. + * + * @brief APIs for the accessing NVIC when using a SoftDevice. + * + */ + +#ifndef NRF_NVIC_H__ +#define NRF_NVIC_H__ + +#include "nrf.h" +#include "nrf_error.h" +#include "nrf_error_soc.h" +#include "nrf_svc.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/**@addtogroup NRF_NVIC_DEFINES Defines + * @{ */ + +/**@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_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. */ \ + )) + +/**@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))) + +/**@brief Interrupts used by the SoftDevice, with IRQn in the range 32-63. */ +#define __NRF_NVIC_SD_IRQS_1 ((uint32_t)0) + +/**@brief Interrupts available for to application, with IRQn in the range 0-31. */ +#define __NRF_NVIC_APP_IRQS_0 (~__NRF_NVIC_SD_IRQS_0) + +/**@brief Interrupts available for to application, with IRQn in the range 32-63. */ +#define __NRF_NVIC_APP_IRQS_1 (~__NRF_NVIC_SD_IRQS_1) + +/**@} */ + +/**@} */ + +/**@addtogroup NRF_NVIC_VARIABLES Variables + * @{ */ + +/**@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 */ +} nrf_nvic_state_t; + +/**@brief Variable keeping the state for the SoftDevice NVIC module. This must be declared in an + * application source file. */ +extern nrf_nvic_state_t nrf_nvic_state; + +/**@} */ + +/**@addtogroup NRF_NVIC_INTERNAL_FUNCTIONS SoftDevice NVIC internal functions + * @{ */ + +/**@brief Disables IRQ interrupts globally, including the SoftDevice's interrupts. + * + * @retval The value of PRIMASK prior to disabling the interrupts. + */ +__STATIC_INLINE int __sd_nvic_irq_disable(void); + +/**@brief Enables IRQ interrupts globally, including the SoftDevice's interrupts. + */ +__STATIC_INLINE void __sd_nvic_irq_enable(void); + +/**@brief Checks if IRQn is available to application + * @param[in] IRQn IRQ to check + * + * @retval 1 (true) if the IRQ to check is available to the application + */ +__STATIC_INLINE uint32_t __sd_nvic_app_accessible_irq(IRQn_Type IRQn); + +/**@brief Checks if priority is available to application + * @param[in] priority priority to check + * + * @retval 1 (true) if the priority to check is available to the application + */ +__STATIC_INLINE uint32_t __sd_nvic_is_app_accessible_priority(uint32_t priority); + +/**@} */ + +/**@addtogroup NRF_NVIC_FUNCTIONS SoftDevice NVIC public functions + * @{ */ + +/**@brief Enable External Interrupt. + * @note Corresponds to NVIC_EnableIRQ in CMSIS. + * + * @pre IRQn is valid and not reserved by the stack. + * + * @param[in] IRQn See the NVIC_EnableIRQ documentation in CMSIS. + * + * @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. + */ +__STATIC_INLINE uint32_t sd_nvic_EnableIRQ(IRQn_Type IRQn); + +/**@brief Disable External Interrupt. + * @note Corresponds to NVIC_DisableIRQ in CMSIS. + * + * @pre IRQn is valid and not reserved by the stack. + * + * @param[in] IRQn See the NVIC_DisableIRQ documentation in CMSIS. + * + * @retval ::NRF_SUCCESS The interrupt was disabled. + * @retval ::NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE The interrupt is not available for the application. + */ +__STATIC_INLINE uint32_t sd_nvic_DisableIRQ(IRQn_Type IRQn); + +/**@brief Get Pending Interrupt. + * @note Corresponds to NVIC_GetPendingIRQ in CMSIS. + * + * @pre IRQn is valid and not reserved by the stack. + * + * @param[in] IRQn See the NVIC_GetPendingIRQ documentation in CMSIS. + * @param[out] p_pending_irq Return value from NVIC_GetPendingIRQ. + * + * @retval ::NRF_SUCCESS The interrupt is available for the application. + * @retval ::NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE IRQn is not available for the application. + */ +__STATIC_INLINE uint32_t sd_nvic_GetPendingIRQ(IRQn_Type IRQn, uint32_t *p_pending_irq); + +/**@brief Set Pending Interrupt. + * @note Corresponds to NVIC_SetPendingIRQ in CMSIS. + * + * @pre IRQn is valid and not reserved by the stack. + * + * @param[in] IRQn See the NVIC_SetPendingIRQ documentation in CMSIS. + * + * @retval ::NRF_SUCCESS The interrupt is set pending. + * @retval ::NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE IRQn is not available for the application. + */ +__STATIC_INLINE uint32_t sd_nvic_SetPendingIRQ(IRQn_Type IRQn); + +/**@brief Clear Pending Interrupt. + * @note Corresponds to NVIC_ClearPendingIRQ in CMSIS. + * + * @pre IRQn is valid and not reserved by the stack. + * + * @param[in] IRQn See the NVIC_ClearPendingIRQ documentation in CMSIS. + * + * @retval ::NRF_SUCCESS The interrupt pending flag is cleared. + * @retval ::NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE IRQn is not available for the application. + */ +__STATIC_INLINE uint32_t sd_nvic_ClearPendingIRQ(IRQn_Type IRQn); + +/**@brief Set Interrupt Priority. + * @note Corresponds to NVIC_SetPriority in CMSIS. + * + * @pre IRQn is valid and not reserved by the stack. + * @pre Priority is valid and not reserved by the stack. + * + * @param[in] IRQn See the NVIC_SetPriority documentation in CMSIS. + * @param[in] priority A valid IRQ priority for use by the application. + * + * @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. + */ +__STATIC_INLINE uint32_t sd_nvic_SetPriority(IRQn_Type IRQn, uint32_t priority); + +/**@brief Get Interrupt Priority. + * @note Corresponds to NVIC_GetPriority in CMSIS. + * + * @pre IRQn is valid and not reserved by the stack. + * + * @param[in] IRQn See the NVIC_GetPriority documentation in CMSIS. + * @param[out] p_priority Return value from NVIC_GetPriority. + * + * @retval ::NRF_SUCCESS The interrupt priority is returned in p_priority. + * @retval ::NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE - IRQn is not available for the application. + */ +__STATIC_INLINE uint32_t sd_nvic_GetPriority(IRQn_Type IRQn, uint32_t *p_priority); + +/**@brief System Reset. + * @note Corresponds to NVIC_SystemReset in CMSIS. + * + * @retval ::NRF_ERROR_SOC_NVIC_SHOULD_NOT_RETURN + */ +__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 + * @sa sd_nvic_critical_region_exit + * + * @param[out] p_is_nested_critical_region If 1, the application is now in a nested critical region. + * + * @retval ::NRF_SUCCESS + */ +__STATIC_INLINE uint32_t sd_nvic_critical_region_enter(uint8_t *p_is_nested_critical_region); + +/**@brief Exit critical region. + * + * @pre Application has entered a critical region using ::sd_nvic_critical_region_enter. + * @post If not in a nested critical region, the application interrupts will restored to the state before + * ::sd_nvic_critical_region_enter was called. + * + * @param[in] is_nested_critical_region If this is set to 1, the critical region won't be exited. @sa + * sd_nvic_critical_region_enter. + * + * @retval ::NRF_SUCCESS + */ +__STATIC_INLINE uint32_t sd_nvic_critical_region_exit(uint8_t is_nested_critical_region); + +/**@} */ + +#ifndef SUPPRESS_INLINE_IMPLEMENTATION + +__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 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 */ + +#ifdef __cplusplus +} +#endif + +#endif // NRF_NVIC_H__ + +/**@} */ diff --git a/src/platform/nrf52/softdevice/nrf_sdm.h b/src/platform/nrf52/softdevice/nrf_sdm.h new file mode 100644 index 0000000..02bf135 --- /dev/null +++ b/src/platform/nrf52/softdevice/nrf_sdm.h @@ -0,0 +1,380 @@ +/* + * Copyright (c) Nordic Semiconductor ASA + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form, except as embedded into a Nordic + * Semiconductor ASA integrated circuit in a product or a software update for + * such product, must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of Nordic Semiconductor ASA nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * 4. This software, with or without modification, must only be used with a + * Nordic Semiconductor ASA integrated circuit. + * + * 5. Any software provided in binary form under this license must not be reverse + * engineered, decompiled, modified and/or disassembled. + * + * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + @defgroup nrf_sdm_api SoftDevice Manager API + @{ + + @brief APIs for SoftDevice management. + +*/ + +#ifndef NRF_SDM_H__ +#define NRF_SDM_H__ + +#include "nrf.h" +#include "nrf_error.h" +#include "nrf_error_sdm.h" +#include "nrf_soc.h" +#include "nrf_svc.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @addtogroup NRF_SDM_DEFINES Defines + * @{ */ +#ifdef NRFSOC_DOXYGEN +/// Declared in nrf_mbr.h +#define MBR_SIZE 0 +#warning test +#endif + +/** @brief The major version for the SoftDevice binary distributed with this header file. */ +#define SD_MAJOR_VERSION (7) + +/** @brief The minor version for the SoftDevice binary distributed with this header file. */ +#define SD_MINOR_VERSION (3) + +/** @brief The bugfix version for the SoftDevice binary distributed with this header file. */ +#define SD_BUGFIX_VERSION (0) + +/** @brief The SoftDevice variant of this firmware. */ +#define SD_VARIANT_ID 140 + +/** @brief The full version number for the SoftDevice binary this header file was distributed + * with, as a decimal number in the form Mmmmbbb, where: + * - M is major version (one or more digits) + * - mmm is minor version (three digits) + * - bbb is bugfix version (three digits). */ +#define SD_VERSION (SD_MAJOR_VERSION * 1000000 + SD_MINOR_VERSION * 1000 + SD_BUGFIX_VERSION) + +/** @brief SoftDevice Manager SVC Base number. */ +#define SDM_SVC_BASE 0x10 + +/** @brief SoftDevice unique string size in bytes. */ +#define SD_UNIQUE_STR_SIZE 20 + +/** @brief Invalid info field. Returned when an info field does not exist. */ +#define SDM_INFO_FIELD_INVALID (0) + +/** @brief Defines the SoftDevice Information Structure location (address) as an offset from +the start of the SoftDevice (without MBR)*/ +#define SOFTDEVICE_INFO_STRUCT_OFFSET (0x2000) + +/** @brief Defines the absolute SoftDevice Information Structure location (address) when the + * SoftDevice is installed just above the MBR (the usual case). */ +#define SOFTDEVICE_INFO_STRUCT_ADDRESS (SOFTDEVICE_INFO_STRUCT_OFFSET + MBR_SIZE) + +/** @brief Defines the offset for the SoftDevice Information Structure size value relative to the + * SoftDevice base address. The size value is of type uint8_t. */ +#define SD_INFO_STRUCT_SIZE_OFFSET (SOFTDEVICE_INFO_STRUCT_OFFSET) + +/** @brief Defines the offset for the SoftDevice size value relative to the SoftDevice base address. + * The size value is of type uint32_t. */ +#define SD_SIZE_OFFSET (SOFTDEVICE_INFO_STRUCT_OFFSET + 0x08) + +/** @brief Defines the offset for FWID value relative to the SoftDevice base address. The FWID value + * is of type uint16_t. */ +#define SD_FWID_OFFSET (SOFTDEVICE_INFO_STRUCT_OFFSET + 0x0C) + +/** @brief Defines the offset for the SoftDevice ID relative to the SoftDevice base address. The ID + * is of type uint32_t. */ +#define SD_ID_OFFSET (SOFTDEVICE_INFO_STRUCT_OFFSET + 0x10) + +/** @brief Defines the offset for the SoftDevice version relative to the SoftDevice base address in + * the same format as @ref SD_VERSION, stored as an uint32_t. */ +#define SD_VERSION_OFFSET (SOFTDEVICE_INFO_STRUCT_OFFSET + 0x14) + +/** @brief Defines the offset for the SoftDevice unique string relative to the SoftDevice base address. + * The SD_UNIQUE_STR is stored as an array of uint8_t. The size of array is @ref SD_UNIQUE_STR_SIZE. + */ +#define SD_UNIQUE_STR_OFFSET (SOFTDEVICE_INFO_STRUCT_OFFSET + 0x18) + +/** @brief Defines a macro for retrieving the actual SoftDevice Information Structure size value + * 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_INFO_STRUCT_SIZE_GET(baseaddr) (*((uint8_t *)((baseaddr) + SD_INFO_STRUCT_SIZE_OFFSET))) + +/** @brief Defines a macro for retrieving the actual SoftDevice size value 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_SIZE_GET(baseaddr) (*((uint32_t *)((baseaddr) + SD_SIZE_OFFSET))) + +/** @brief Defines the amount of flash that is used by the SoftDevice. + * Add @ref MBR_SIZE to find the first available flash address when the SoftDevice is installed + * just above the MBR (the usual case). + */ +#define SD_FLASH_SIZE 0x27000 + +/** @brief Defines a macro for retrieving the actual FWID value 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_FWID_GET(baseaddr) (*((uint16_t *)((baseaddr) + SD_FWID_OFFSET))) + +/** @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) + +/** @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) + +/** @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) + +/**@defgroup NRF_FAULT_ID_RANGES Fault ID ranges + * @{ */ +#define NRF_FAULT_ID_SD_RANGE_START 0x00000000 /**< SoftDevice ID range start. */ +#define NRF_FAULT_ID_APP_RANGE_START 0x00001000 /**< Application ID range start. */ +/**@} */ + +/**@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. */ +/**@} */ + +/** @} */ + +/** @addtogroup NRF_SDM_ENUMS Enumerations + * @{ */ + +/**@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 */ +}; + +/** @} */ + +/** @addtogroup NRF_SDM_DEFINES Defines + * @{ */ + +/**@defgroup NRF_CLOCK_LF_ACCURACY Clock accuracy + * @{ */ + +#define NRF_CLOCK_LF_ACCURACY_250_PPM (0) /**< Default: 250 ppm */ +#define NRF_CLOCK_LF_ACCURACY_500_PPM (1) /**< 500 ppm */ +#define NRF_CLOCK_LF_ACCURACY_150_PPM (2) /**< 150 ppm */ +#define NRF_CLOCK_LF_ACCURACY_100_PPM (3) /**< 100 ppm */ +#define NRF_CLOCK_LF_ACCURACY_75_PPM (4) /**< 75 ppm */ +#define NRF_CLOCK_LF_ACCURACY_50_PPM (5) /**< 50 ppm */ +#define NRF_CLOCK_LF_ACCURACY_30_PPM (6) /**< 30 ppm */ +#define NRF_CLOCK_LF_ACCURACY_20_PPM (7) /**< 20 ppm */ +#define NRF_CLOCK_LF_ACCURACY_10_PPM (8) /**< 10 ppm */ +#define NRF_CLOCK_LF_ACCURACY_5_PPM (9) /**< 5 ppm */ +#define NRF_CLOCK_LF_ACCURACY_2_PPM (10) /**< 2 ppm */ +#define NRF_CLOCK_LF_ACCURACY_1_PPM (11) /**< 1 ppm */ + +/** @} */ + +/**@defgroup NRF_CLOCK_LF_SRC Possible LFCLK oscillator sources + * @{ */ + +#define NRF_CLOCK_LF_SRC_RC (0) /**< LFCLK RC oscillator. */ +#define NRF_CLOCK_LF_SRC_XTAL (1) /**< LFCLK crystal oscillator. */ +#define NRF_CLOCK_LF_SRC_SYNTH (2) /**< LFCLK Synthesized from HFCLK. */ + +/** @} */ + +/** @} */ + +/** @addtogroup NRF_SDM_TYPES Types + * @{ */ + +/**@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. + + @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 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. + * + * When certain unrecoverable errors occur within the application or SoftDevice the fault handler will be called back. + * The protocol stack will be in an undefined state when this happens and the only way to recover will be to + * perform a reset, using e.g. CMSIS NVIC_SystemReset(). + * If the application returns from the fault handler the SoftDevice will call NVIC_SystemReset(). + * + * @note It is recommended to either perform a reset in the fault handler or to let the SoftDevice reset the device. + * Otherwise SoC peripherals may behave in an undefined way. For example, the RADIO peripherial may + * continously transmit packets. + * + * @note This callback is executed in HardFault context, thus SVC functions cannot be called from the fault callback. + * + * @param[in] id Fault identifier. See @ref NRF_FAULT_IDS. + * @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. + */ +typedef void (*nrf_fault_handler_t)(uint32_t id, uint32_t pc, uint32_t info); + +/** @} */ + +/** @addtogroup NRF_SDM_FUNCTIONS Functions + * @{ */ + +/**@brief Enables the SoftDevice and by extension the protocol stack. + * + * @note Some care must be taken if a low frequency clock source is already running when calling this function: + * If the LF clock has a different source then the one currently running, it will be stopped. Then, the new + * clock source will be started. + * + * @note This function has no effect when returning with an error. + * + * @post If return code is ::NRF_SUCCESS + * - SoC library and protocol stack APIs are made available. + * - A portion of RAM will be unavailable (see relevant SDS documentation). + * - Some peripherals will be unavailable or available only through the SoC API (see relevant SDS documentation). + * - Interrupts will not arrive from protected peripherals or interrupts. + * - nrf_nvic_ functions must be used instead of CMSIS NVIC_ functions for reliable usage of the SoftDevice. + * - Interrupt latency may be affected by the SoftDevice (see relevant SDS documentation). + * - Chosen low frequency clock source will be running. + * + * @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. + * @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_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)); + +/**@brief Disables the SoftDevice and by extension the protocol stack. + * + * Idempotent function to disable the SoftDevice. + * + * @post SoC library and protocol stack APIs are made unavailable. + * @post All interrupts that was protected by the SoftDevice will be disabled and initialized to priority 0 (highest). + * @post All peripherals used by the SoftDevice will be reset to default values. + * @post All of RAM become available. + * @post All interrupts are forwarded to the application. + * @post LFCLK source chosen in ::sd_softdevice_enable will be left running. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_SOFTDEVICE_DISABLE, uint32_t, sd_softdevice_disable(void)); + +/**@brief Check if the SoftDevice is enabled. + * + * @param[out] p_softdevice_enabled If the SoftDevice is enabled: 1 else 0. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_SOFTDEVICE_IS_ENABLED, uint32_t, sd_softdevice_is_enabled(uint8_t *p_softdevice_enabled)); + +/**@brief Sets the base address of the interrupt vector table for interrupts forwarded from the SoftDevice + * + * This function is only intended to be called when a bootloader is enabled. + * + * @param[in] address The base address of the interrupt vector table for forwarded interrupts. + + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_SOFTDEVICE_VECTOR_TABLE_BASE_SET, uint32_t, sd_softdevice_vector_table_base_set(uint32_t address)); + +/** @} */ + +#ifdef __cplusplus +} +#endif +#endif // NRF_SDM_H__ + +/** + @} +*/ \ No newline at end of file diff --git a/src/platform/nrf52/softdevice/nrf_soc.h b/src/platform/nrf52/softdevice/nrf_soc.h new file mode 100644 index 0000000..c649ca8 --- /dev/null +++ b/src/platform/nrf52/softdevice/nrf_soc.h @@ -0,0 +1,1046 @@ +/* + * Copyright (c) Nordic Semiconductor ASA + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form, except as embedded into a Nordic + * Semiconductor ASA integrated circuit in a product or a software update for + * such product, must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of Nordic Semiconductor ASA nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * 4. This software, with or without modification, must only be used with a + * Nordic Semiconductor ASA integrated circuit. + * + * 5. Any software provided in binary form under this license must not be reverse + * engineered, decompiled, modified and/or disassembled. + * + * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @defgroup nrf_soc_api SoC Library API + * @{ + * + * @brief APIs for the SoC library. + * + */ + +#ifndef NRF_SOC_H__ +#define NRF_SOC_H__ + +#include "nrf.h" +#include "nrf_error.h" +#include "nrf_error_soc.h" +#include "nrf_svc.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/**@addtogroup NRF_SOC_DEFINES Defines + * @{ */ + +/**@brief The number of the lowest SVC number reserved for the SoC library. */ +#define SOC_SVC_BASE (0x20) /**< Base value for SVCs that are available when the SoftDevice is disabled. */ +#define SOC_SVC_BASE_NOT_AVAILABLE (0x2C) /**< Base value for SVCs that are not available when the SoftDevice is disabled. */ + +/**@brief Guaranteed time for application to process radio inactive notification. */ +#define NRF_RADIO_NOTIFICATION_INACTIVE_GUARANTEED_TIME_US (62) + +/**@brief The minimum allowed timeslot extension time. */ +#define NRF_RADIO_MINIMUM_TIMESLOT_LENGTH_EXTENSION_TIME_US (200) + +/**@brief The maximum processing time to handle a timeslot extension. */ +#define NRF_RADIO_MAX_EXTENSION_PROCESSING_TIME_US (20) + +/**@brief The latest time before the end of a timeslot the timeslot can be extended. */ +#define NRF_RADIO_MIN_EXTENSION_MARGIN_US (82) + +#define SOC_ECB_KEY_LENGTH (16) /**< ECB key length. */ +#define SOC_ECB_CLEARTEXT_LENGTH (16) /**< ECB cleartext length. */ +#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. \ + 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. \ + 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_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. */ + +/**@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))) + +/**@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)) + +/**@brief Mask of PPI groups reserved by the SoftDevice when the SoftDevice is enabled. */ +#define NRF_SOC_SD_PPI_GROUPS_SD_ENABLED_MSK ((uint32_t)((1U << 4) | (1U << 5))) + +/**@} */ + +/**@addtogroup NRF_SOC_ENUMS Enumerations + * @{ */ + +/**@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 +}; + +/**@brief Possible values of a ::nrf_mutex_t. */ +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. */ +}; + +/**@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. */ +}; + +/**@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. */ +}; + +/**@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. */ +}; + +/**@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. */ +}; + +/**@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. */ +}; + +/**@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. */ +}; + +/**@brief The actions requested by the signal callback. + * + * This code gives the SOC instructions about what action to take when the signal callback has + * 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. */ +}; + +/**@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. */ +}; + +/**@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)). */ +}; + +/**@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. */ +}; + +/**@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 +}; + +/**@} */ + +/**@addtogroup NRF_SOC_STRUCTURES Structures + * @{ */ + +/**@brief Represents a mutex for use with the nrf_mutex functions. + * @note Accessing the value directly is not safe, use the mutex functions! + */ +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). */ +} 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). */ +} 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. */ +} 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. */ +} nrf_radio_signal_callback_return_param_t; + +/**@brief The radio timeslot signal callback type. + * + * @note In case of invalid return parameters, the radio timeslot will automatically end + * immediately after returning from the signal callback and the + * @ref NRF_EVT_RADIO_SIGNAL_CALLBACK_INVALID_RETURN event will be sent. + * @note The returned struct pointer must remain valid after the signal callback + * function returns. For instance, this means that it must not point to a stack variable. + * + * @param[in] signal_type Type of signal, see @ref NRF_RADIO_CALLBACK_SIGNAL_TYPE. + * + * @return Pointer to structure containing action requested by the application. + */ +typedef nrf_radio_signal_callback_return_param_t *(*nrf_radio_signal_callback_t)(uint8_t signal_type); + +/**@brief AES ECB parameter typedefs */ +typedef uint8_t soc_ecb_key_t[SOC_ECB_KEY_LENGTH]; /**< Encryption key type. */ +typedef uint8_t soc_ecb_cleartext_t[SOC_ECB_CLEARTEXT_LENGTH]; /**< Cleartext data type. */ +typedef uint8_t soc_ecb_ciphertext_t[SOC_ECB_CIPHERTEXT_LENGTH]; /**< Ciphertext data type. */ + +/**@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. */ +} 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. */ +} nrf_ecb_hal_data_block_t; + +/**@} */ + +/**@addtogroup NRF_SOC_FUNCTIONS Functions + * @{ */ + +/**@brief Initialize a mutex. + * + * @param[in] p_mutex Pointer to the mutex to initialize. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_MUTEX_NEW, uint32_t, sd_mutex_new(nrf_mutex_t *p_mutex)); + +/**@brief Attempt to acquire a mutex. + * + * @param[in] p_mutex Pointer to the mutex to acquire. + * + * @retval ::NRF_SUCCESS The mutex was successfully acquired. + * @retval ::NRF_ERROR_SOC_MUTEX_ALREADY_TAKEN The mutex could not be acquired. + */ +SVCALL(SD_MUTEX_ACQUIRE, uint32_t, sd_mutex_acquire(nrf_mutex_t *p_mutex)); + +/**@brief Release a mutex. + * + * @param[in] p_mutex Pointer to the mutex to release. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_MUTEX_RELEASE, uint32_t, sd_mutex_release(nrf_mutex_t *p_mutex)); + +/**@brief Query the capacity of the application random pool. + * + * @param[out] p_pool_capacity The capacity of the pool. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_RAND_APPLICATION_POOL_CAPACITY_GET, uint32_t, sd_rand_application_pool_capacity_get(uint8_t *p_pool_capacity)); + +/**@brief Get number of random bytes available to the application. + * + * @param[out] p_bytes_available The number of bytes currently available in the pool. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_RAND_APPLICATION_BYTES_AVAILABLE_GET, uint32_t, sd_rand_application_bytes_available_get(uint8_t *p_bytes_available)); + +/**@brief Get random bytes from the application pool. + * + * @param[out] p_buff Pointer to unit8_t buffer for storing the bytes. + * @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. + */ +SVCALL(SD_RAND_APPLICATION_VECTOR_GET, uint32_t, sd_rand_application_vector_get(uint8_t *p_buff, uint8_t length)); + +/**@brief Gets the reset reason register. + * + * @param[out] p_reset_reason Contents of the NRF_POWER->RESETREAS register. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_POWER_RESET_REASON_GET, uint32_t, sd_power_reset_reason_get(uint32_t *p_reset_reason)); + +/**@brief Clears the bits of the reset reason register. + * + * @param[in] reset_reason_clr_msk Contains the bits to clear from the reset reason register. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_POWER_RESET_REASON_CLR, uint32_t, sd_power_reset_reason_clr(uint32_t reset_reason_clr_msk)); + +/**@brief Sets the power mode when in CPU sleep. + * + * @param[in] power_mode The power mode to use when in CPU sleep, see @ref NRF_POWER_MODES. @sa sd_app_evt_wait + * + * @retval ::NRF_SUCCESS The power mode was set. + * @retval ::NRF_ERROR_SOC_POWER_MODE_UNKNOWN The power mode was unknown. + */ +SVCALL(SD_POWER_MODE_SET, uint32_t, sd_power_mode_set(uint8_t power_mode)); + +/**@brief Puts the chip in System OFF mode. + * + * @retval ::NRF_ERROR_SOC_POWER_OFF_SHOULD_NOT_RETURN + */ +SVCALL(SD_POWER_SYSTEM_OFF, uint32_t, sd_power_system_off(void)); + +/**@brief Enables or disables the power-fail comparator. + * + * Enabling this will give a SoftDevice event (NRF_EVT_POWER_FAILURE_WARNING) when the power failure warning occurs. + * The event can be retrieved with sd_evt_get(); + * + * @param[in] pof_enable True if the power-fail comparator should be enabled, false if it should be disabled. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_POWER_POF_ENABLE, uint32_t, sd_power_pof_enable(uint8_t pof_enable)); + +/**@brief Enables or disables the USB power ready event. + * + * Enabling this will give a SoftDevice event (NRF_EVT_POWER_USB_POWER_READY) when a USB 3.3 V supply is ready. + * The event can be retrieved with sd_evt_get(); + * + * @param[in] usbpwrrdy_enable True if the power ready event should be enabled, false if it should be disabled. + * + * @note Calling this function on a chip without USBD peripheral will result in undefined behaviour. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_POWER_USBPWRRDY_ENABLE, uint32_t, sd_power_usbpwrrdy_enable(uint8_t usbpwrrdy_enable)); + +/**@brief Enables or disables the power USB-detected event. + * + * Enabling this will give a SoftDevice event (NRF_EVT_POWER_USB_DETECTED) when a voltage supply is detected on VBUS. + * The event can be retrieved with sd_evt_get(); + * + * @param[in] usbdetected_enable True if the power ready event should be enabled, false if it should be disabled. + * + * @note Calling this function on a chip without USBD peripheral will result in undefined behaviour. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_POWER_USBDETECTED_ENABLE, uint32_t, sd_power_usbdetected_enable(uint8_t usbdetected_enable)); + +/**@brief Enables or disables the power USB-removed event. + * + * Enabling this will give a SoftDevice event (NRF_EVT_POWER_USB_REMOVED) when a voltage supply is removed from VBUS. + * The event can be retrieved with sd_evt_get(); + * + * @param[in] usbremoved_enable True if the power ready event should be enabled, false if it should be disabled. + * + * @note Calling this function on a chip without USBD peripheral will result in undefined behaviour. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_POWER_USBREMOVED_ENABLE, uint32_t, sd_power_usbremoved_enable(uint8_t usbremoved_enable)); + +/**@brief Get USB supply status register content. + * + * @param[out] usbregstatus The content of USBREGSTATUS register. + * + * @note Calling this function on a chip without USBD peripheral will result in undefined behaviour. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_POWER_USBREGSTATUS_GET, uint32_t, sd_power_usbregstatus_get(uint32_t *usbregstatus)); + +/**@brief Sets the power failure comparator threshold value. + * + * @note: Power failure comparator threshold setting. This setting applies both for normal voltage + * mode (supply connected to both VDD and VDDH) and high voltage mode (supply connected to + * VDDH only). + * + * @param[in] threshold The power-fail threshold value to use, see @ref NRF_POWER_THRESHOLDS. + * + * @retval ::NRF_SUCCESS The power failure threshold was set. + * @retval ::NRF_ERROR_SOC_POWER_POF_THRESHOLD_UNKNOWN The power failure threshold is unknown. + */ +SVCALL(SD_POWER_POF_THRESHOLD_SET, uint32_t, sd_power_pof_threshold_set(uint8_t threshold)); + +/**@brief Sets the power failure comparator threshold value for high voltage. + * + * @note: Power failure comparator threshold setting for high voltage mode (supply connected to + * VDDH only). This setting does not apply for normal voltage mode (supply connected to both + * VDD and VDDH). + * + * @param[in] threshold The power-fail threshold value to use, see @ref NRF_POWER_THRESHOLDVDDHS. + * + * @retval ::NRF_SUCCESS The power failure threshold was set. + * @retval ::NRF_ERROR_SOC_POWER_POF_THRESHOLD_UNKNOWN The power failure threshold is unknown. + */ +SVCALL(SD_POWER_POF_THRESHOLDVDDH_SET, uint32_t, sd_power_pof_thresholdvddh_set(uint8_t threshold)); + +/**@brief Writes the NRF_POWER->RAM[index].POWERSET register. + * + * @param[in] index Contains the index in the NRF_POWER->RAM[index].POWERSET register to write to. + * @param[in] ram_powerset Contains the word to write to the NRF_POWER->RAM[index].POWERSET register. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_POWER_RAM_POWER_SET, uint32_t, sd_power_ram_power_set(uint8_t index, uint32_t ram_powerset)); + +/**@brief Writes the NRF_POWER->RAM[index].POWERCLR register. + * + * @param[in] index Contains the index in the NRF_POWER->RAM[index].POWERCLR register to write to. + * @param[in] ram_powerclr Contains the word to write to the NRF_POWER->RAM[index].POWERCLR register. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_POWER_RAM_POWER_CLR, uint32_t, sd_power_ram_power_clr(uint8_t index, uint32_t ram_powerclr)); + +/**@brief Get contents of NRF_POWER->RAM[index].POWER register, indicates power status of RAM[index] blocks. + * + * @param[in] index Contains the index in the NRF_POWER->RAM[index].POWER register to read from. + * @param[out] p_ram_power Content of NRF_POWER->RAM[index].POWER register. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_POWER_RAM_POWER_GET, uint32_t, sd_power_ram_power_get(uint8_t index, uint32_t *p_ram_power)); + +/**@brief Set bits in the general purpose retention registers (NRF_POWER->GPREGRET*). + * + * @param[in] gpregret_id 0 for GPREGRET, 1 for GPREGRET2. + * @param[in] gpregret_msk Bits to be set in the GPREGRET register. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_POWER_GPREGRET_SET, uint32_t, sd_power_gpregret_set(uint32_t gpregret_id, uint32_t gpregret_msk)); + +/**@brief Clear bits in the general purpose retention registers (NRF_POWER->GPREGRET*). + * + * @param[in] gpregret_id 0 for GPREGRET, 1 for GPREGRET2. + * @param[in] gpregret_msk Bits to be clear in the GPREGRET register. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_POWER_GPREGRET_CLR, uint32_t, sd_power_gpregret_clr(uint32_t gpregret_id, uint32_t gpregret_msk)); + +/**@brief Get contents of the general purpose retention registers (NRF_POWER->GPREGRET*). + * + * @param[in] gpregret_id 0 for GPREGRET, 1 for GPREGRET2. + * @param[out] p_gpregret Contents of the GPREGRET register. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_POWER_GPREGRET_GET, uint32_t, sd_power_gpregret_get(uint32_t gpregret_id, uint32_t *p_gpregret)); + +/**@brief Enable or disable the DC/DC regulator for the regulator stage 1 (REG1). + * + * @param[in] dcdc_mode The mode of the DCDC, see @ref NRF_POWER_DCDC_MODES. + * + * @retval ::NRF_SUCCESS + * @retval ::NRF_ERROR_INVALID_PARAM The DCDC mode is invalid. + */ +SVCALL(SD_POWER_DCDC_MODE_SET, uint32_t, sd_power_dcdc_mode_set(uint8_t dcdc_mode)); + +/**@brief Enable or disable the DC/DC regulator for the regulator stage 0 (REG0). + * + * For more details on the REG0 stage, please see product specification. + * + * @param[in] dcdc_mode The mode of the DCDC0, see @ref NRF_POWER_DCDC_MODES. + * + * @retval ::NRF_SUCCESS + * @retval ::NRF_ERROR_INVALID_PARAM The dcdc_mode is invalid. + */ +SVCALL(SD_POWER_DCDC0_MODE_SET, uint32_t, sd_power_dcdc0_mode_set(uint8_t dcdc_mode)); + +/**@brief Request the high frequency crystal oscillator. + * + * Will start the high frequency crystal oscillator, the startup time of the crystal varies + * and the ::sd_clock_hfclk_is_running function can be polled to check if it has started. + * + * @see sd_clock_hfclk_is_running + * @see sd_clock_hfclk_release + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_CLOCK_HFCLK_REQUEST, uint32_t, sd_clock_hfclk_request(void)); + +/**@brief Releases the high frequency crystal oscillator. + * + * Will stop the high frequency crystal oscillator, this happens immediately. + * + * @see sd_clock_hfclk_is_running + * @see sd_clock_hfclk_request + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_CLOCK_HFCLK_RELEASE, uint32_t, sd_clock_hfclk_release(void)); + +/**@brief Checks if the high frequency crystal oscillator is running. + * + * @see sd_clock_hfclk_request + * @see sd_clock_hfclk_release + * + * @param[out] p_is_running 1 if the external crystal oscillator is running, 0 if not. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_CLOCK_HFCLK_IS_RUNNING, uint32_t, sd_clock_hfclk_is_running(uint32_t *p_is_running)); + +/**@brief Waits for an application event. + * + * An application event is either an application interrupt or a pended interrupt when the interrupt + * is disabled. + * + * When the application waits for an application event by calling this function, an interrupt that + * is enabled will be taken immediately on pending since this function will wait in thread mode, + * then the execution will return in the application's main thread. + * + * In order to wake up from disabled interrupts, the SEVONPEND flag has to be set in the Cortex-M + * MCU's System Control Register (SCR), CMSIS_SCB. In that case, when a disabled interrupt gets + * pended, this function will return to the application's main thread. + * + * @note The application must ensure that the pended flag is cleared using ::sd_nvic_ClearPendingIRQ + * in order to sleep using this function. This is only necessary for disabled interrupts, as + * the interrupt handler will clear the pending flag automatically for enabled interrupts. + * + * @note If an application interrupt has happened since the last time sd_app_evt_wait was + * called this function will return immediately and not go to sleep. This is to avoid race + * conditions that can occur when a flag is updated in the interrupt handler and processed + * in the main loop. + * + * @post An application interrupt has happened or a interrupt pending flag is set. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_APP_EVT_WAIT, uint32_t, sd_app_evt_wait(void)); + +/**@brief Get PPI channel enable register contents. + * + * @param[out] p_channel_enable The contents of the PPI CHEN register. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_PPI_CHANNEL_ENABLE_GET, uint32_t, sd_ppi_channel_enable_get(uint32_t *p_channel_enable)); + +/**@brief Set PPI channel enable register. + * + * @param[in] channel_enable_set_msk Mask containing the bits to set in the PPI CHEN register. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_PPI_CHANNEL_ENABLE_SET, uint32_t, sd_ppi_channel_enable_set(uint32_t channel_enable_set_msk)); + +/**@brief Clear PPI channel enable register. + * + * @param[in] channel_enable_clr_msk Mask containing the bits to clear in the PPI CHEN register. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_PPI_CHANNEL_ENABLE_CLR, uint32_t, sd_ppi_channel_enable_clr(uint32_t channel_enable_clr_msk)); + +/**@brief Assign endpoints to a PPI channel. + * + * @param[in] channel_num Number of the PPI channel to assign. + * @param[in] evt_endpoint Event endpoint of the PPI channel. + * @param[in] task_endpoint Task endpoint of the PPI channel. + * + * @retval ::NRF_ERROR_SOC_PPI_INVALID_CHANNEL The channel number is invalid. + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_PPI_CHANNEL_ASSIGN, uint32_t, + sd_ppi_channel_assign(uint8_t channel_num, const volatile void *evt_endpoint, const volatile void *task_endpoint)); + +/**@brief Task to enable a channel group. + * + * @param[in] group_num Number of the channel group. + * + * @retval ::NRF_ERROR_SOC_PPI_INVALID_GROUP The group number is invalid + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_PPI_GROUP_TASK_ENABLE, uint32_t, sd_ppi_group_task_enable(uint8_t group_num)); + +/**@brief Task to disable a channel group. + * + * @param[in] group_num Number of the PPI group. + * + * @retval ::NRF_ERROR_SOC_PPI_INVALID_GROUP The group number is invalid. + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_PPI_GROUP_TASK_DISABLE, uint32_t, sd_ppi_group_task_disable(uint8_t group_num)); + +/**@brief Assign PPI channels to a channel group. + * + * @param[in] group_num Number of the channel group. + * @param[in] channel_msk Mask of the channels to assign to the group. + * + * @retval ::NRF_ERROR_SOC_PPI_INVALID_GROUP The group number is invalid. + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_PPI_GROUP_ASSIGN, uint32_t, sd_ppi_group_assign(uint8_t group_num, uint32_t channel_msk)); + +/**@brief Gets the PPI channels of a channel group. + * + * @param[in] group_num Number of the channel group. + * @param[out] p_channel_msk Mask of the channels assigned to the group. + * + * @retval ::NRF_ERROR_SOC_PPI_INVALID_GROUP The group number is invalid. + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_PPI_GROUP_GET, uint32_t, sd_ppi_group_get(uint8_t group_num, uint32_t *p_channel_msk)); + +/**@brief Configures the Radio Notification signal. + * + * @note + * - The notification signal latency depends on the interrupt priority settings of SWI used + * for notification signal. + * - To ensure that the radio notification signal behaves in a consistent way, the radio + * notifications must be configured when there is no protocol stack or other SoftDevice + * activity in progress. It is recommended that the radio notification signal is + * configured directly after the SoftDevice has been enabled. + * - In the period between the ACTIVE signal and the start of the Radio Event, the SoftDevice + * will interrupt the application to do Radio Event preparation. + * - Using the Radio Notification feature may limit the bandwidth, as the SoftDevice may have + * to shorten the connection events to have time for the Radio Notification signals. + * + * @param[in] type Type of notification signal, see @ref NRF_RADIO_NOTIFICATION_TYPES. + * @ref NRF_RADIO_NOTIFICATION_TYPE_NONE shall be used to turn off radio + * notification. Using @ref NRF_RADIO_NOTIFICATION_DISTANCE_NONE is + * recommended (but not required) to be used with + * @ref NRF_RADIO_NOTIFICATION_TYPE_NONE. + * + * @param[in] distance Distance between the notification signal and start of radio activity, see @ref + * NRF_RADIO_NOTIFICATION_DISTANCES. This parameter is ignored when @ref NRF_RADIO_NOTIFICATION_TYPE_NONE or + * @ref NRF_RADIO_NOTIFICATION_TYPE_INT_ON_INACTIVE is used. + * + * @retval ::NRF_ERROR_INVALID_PARAM The group number is invalid. + * @retval ::NRF_ERROR_INVALID_STATE A protocol stack or other SoftDevice is running. Stop all + * running activities and retry. + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_RADIO_NOTIFICATION_CFG_SET, uint32_t, sd_radio_notification_cfg_set(uint8_t type, uint8_t distance)); + +/**@brief Encrypts a block according to the specified parameters. + * + * 128-bit AES encryption. + * + * @note: + * - The application may set the SEVONPEND bit in the SCR to 1 to make the SoftDevice sleep while + * the ECB is running. The SEVONPEND bit should only be cleared (set to 0) from application + * main or low interrupt level. + * + * @param[in, out] p_ecb_data Pointer to the ECB parameters' struct (two input + * parameters and one output parameter). + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_ECB_BLOCK_ENCRYPT, uint32_t, sd_ecb_block_encrypt(nrf_ecb_hal_data_t *p_ecb_data)); + +/**@brief Encrypts multiple data blocks provided as an array of data block structures. + * + * @details: Performs 128-bit AES encryption on multiple data blocks + * + * @note: + * - The application may set the SEVONPEND bit in the SCR to 1 to make the SoftDevice sleep while + * the ECB is running. The SEVONPEND bit should only be cleared (set to 0) from application + * main or low interrupt level. + * + * @param[in] block_count Count of blocks in the p_data_blocks array. + * @param[in,out] p_data_blocks Pointer to the first entry in a contiguous array of + * @ref nrf_ecb_hal_data_block_t structures. + * + * @retval ::NRF_SUCCESS + */ +SVCALL(SD_ECB_BLOCKS_ENCRYPT, uint32_t, sd_ecb_blocks_encrypt(uint8_t block_count, nrf_ecb_hal_data_block_t *p_data_blocks)); + +/**@brief Gets any pending events generated by the SoC API. + * + * The application should keep calling this function to get events, until ::NRF_ERROR_NOT_FOUND is returned. + * + * @param[out] p_evt_id Set to one of the values in @ref NRF_SOC_EVTS, if any events are pending. + * + * @retval ::NRF_SUCCESS An event was pending. The event id is written in the p_evt_id parameter. + * @retval ::NRF_ERROR_NOT_FOUND No pending events. + */ +SVCALL(SD_EVT_GET, uint32_t, sd_evt_get(uint32_t *p_evt_id)); + +/**@brief Get the temperature measured on the chip + * + * This function will block until the temperature measurement is done. + * It takes around 50 us from call to return. + * + * @param[out] p_temp Result of temperature measurement. Die temperature in 0.25 degrees Celsius. + * + * @retval ::NRF_SUCCESS A temperature measurement was done, and the temperature was written to temp + */ +SVCALL(SD_TEMP_GET, uint32_t, sd_temp_get(int32_t *p_temp)); + +/**@brief Flash Write + * + * Commands to write a buffer to flash + * + * If the SoftDevice is enabled: + * This call initiates the flash access command, and its completion will be communicated to the + * application with exactly one of the following events: + * - @ref NRF_EVT_FLASH_OPERATION_SUCCESS - The command was successfully completed. + * - @ref NRF_EVT_FLASH_OPERATION_ERROR - The command could not be started. + * + * If the SoftDevice is not enabled no event will be generated, and this call will return @ref NRF_SUCCESS when the + * write has been completed + * + * @note + * - This call takes control over the radio and the CPU during flash erase and write to make sure that + * they will not interfere with the flash access. This means that all interrupts will be blocked + * for a predictable time (depending on the NVMC specification in the device's Product Specification + * and the command parameters). + * - The data in the p_src buffer should not be modified before the @ref NRF_EVT_FLASH_OPERATION_SUCCESS + * or the @ref NRF_EVT_FLASH_OPERATION_ERROR have been received if the SoftDevice is enabled. + * - This call will make the SoftDevice trigger a hardfault when the page is written, if it is + * protected. + * + * + * @param[in] p_dst Pointer to start of flash location to be written. + * @param[in] p_src Pointer to buffer with data to be written. + * @param[in] size Number of 32-bit words to write. Maximum size is the number of words in one + * flash page. See the device's Product Specification for details. + * + * @retval ::NRF_ERROR_INVALID_ADDR Tried to write to a non existing flash address, or p_dst or p_src was unaligned. + * @retval ::NRF_ERROR_BUSY The previous command has not yet completed. + * @retval ::NRF_ERROR_INVALID_LENGTH Size was 0, or higher than the maximum allowed size. + * @retval ::NRF_ERROR_FORBIDDEN Tried to write to an address outside the application flash area. + * @retval ::NRF_SUCCESS The command was accepted. + */ +SVCALL(SD_FLASH_WRITE, uint32_t, sd_flash_write(uint32_t *p_dst, uint32_t const *p_src, uint32_t size)); + +/**@brief Flash Erase page + * + * Commands to erase a flash page + * If the SoftDevice is enabled: + * This call initiates the flash access command, and its completion will be communicated to the + * application with exactly one of the following events: + * - @ref NRF_EVT_FLASH_OPERATION_SUCCESS - The command was successfully completed. + * - @ref NRF_EVT_FLASH_OPERATION_ERROR - The command could not be started. + * + * If the SoftDevice is not enabled no event will be generated, and this call will return @ref NRF_SUCCESS when the + * erase has been completed + * + * @note + * - This call takes control over the radio and the CPU during flash erase and write to make sure that + * they will not interfere with the flash access. This means that all interrupts will be blocked + * for a predictable time (depending on the NVMC specification in the device's Product Specification + * and the command parameters). + * - This call will make the SoftDevice trigger a hardfault when the page is erased, if it is + * protected. + * + * + * @param[in] page_number Page number of the page to erase + * + * @retval ::NRF_ERROR_INTERNAL If a new session could not be opened due to an internal error. + * @retval ::NRF_ERROR_INVALID_ADDR Tried to erase to a non existing flash page. + * @retval ::NRF_ERROR_BUSY The previous command has not yet completed. + * @retval ::NRF_ERROR_FORBIDDEN Tried to erase a page outside the application flash area. + * @retval ::NRF_SUCCESS The command was accepted. + */ +SVCALL(SD_FLASH_PAGE_ERASE, uint32_t, sd_flash_page_erase(uint32_t page_number)); + +/**@brief Opens a session for radio timeslot requests. + * + * @note Only one session can be open at a time. + * @note p_radio_signal_callback(@ref NRF_RADIO_CALLBACK_SIGNAL_TYPE_START) will be called when the radio timeslot + * starts. From this point the NRF_RADIO and NRF_TIMER0 peripherals can be freely accessed + * by the application. + * @note p_radio_signal_callback(@ref NRF_RADIO_CALLBACK_SIGNAL_TYPE_TIMER0) is called whenever the NRF_TIMER0 + * interrupt occurs. + * @note p_radio_signal_callback(@ref NRF_RADIO_CALLBACK_SIGNAL_TYPE_RADIO) is called whenever the NRF_RADIO + * interrupt occurs. + * @note p_radio_signal_callback() will be called at ARM interrupt priority level 0. This + * implies that none of the sd_* API calls can be used from p_radio_signal_callback(). + * + * @param[in] p_radio_signal_callback The signal callback. + * + * @retval ::NRF_ERROR_INVALID_ADDR p_radio_signal_callback is an invalid function pointer. + * @retval ::NRF_ERROR_BUSY If session cannot be opened. + * @retval ::NRF_ERROR_INTERNAL If a new session could not be opened due to an internal error. + * @retval ::NRF_SUCCESS Otherwise. + */ +SVCALL(SD_RADIO_SESSION_OPEN, uint32_t, sd_radio_session_open(nrf_radio_signal_callback_t p_radio_signal_callback)); + +/**@brief Closes a session for radio timeslot requests. + * + * @note Any current radio timeslot will be finished before the session is closed. + * @note If a radio timeslot is scheduled when the session is closed, it will be canceled. + * @note The application cannot consider the session closed until the @ref NRF_EVT_RADIO_SESSION_CLOSED + * event is received. + * + * @retval ::NRF_ERROR_FORBIDDEN If session not opened. + * @retval ::NRF_ERROR_BUSY If session is currently being closed. + * @retval ::NRF_SUCCESS Otherwise. + */ +SVCALL(SD_RADIO_SESSION_CLOSE, uint32_t, sd_radio_session_close(void)); + +/**@brief Requests a radio timeslot. + * + * @note The request type is determined by p_request->request_type, and can be one of @ref NRF_RADIO_REQ_TYPE_EARLIEST + * and @ref NRF_RADIO_REQ_TYPE_NORMAL. The first request in a session must always be of type @ref + * NRF_RADIO_REQ_TYPE_EARLIEST. + * @note For a normal request (@ref NRF_RADIO_REQ_TYPE_NORMAL), the start time of a radio timeslot is specified by + * p_request->distance_us and is given relative to the start of the previous timeslot. + * @note A too small p_request->distance_us will lead to a @ref NRF_EVT_RADIO_BLOCKED event. + * @note Timeslots scheduled too close will lead to a @ref NRF_EVT_RADIO_BLOCKED event. + * @note See the SoftDevice Specification for more on radio timeslot scheduling, distances and lengths. + * @note If an opportunity for the first radio timeslot is not found before 100 ms after the call to this + * function, it is not scheduled, and instead a @ref NRF_EVT_RADIO_BLOCKED event is sent. + * The application may then try to schedule the first radio timeslot again. + * @note Successful requests will result in nrf_radio_signal_callback_t(@ref NRF_RADIO_CALLBACK_SIGNAL_TYPE_START). + * Unsuccessful requests will result in a @ref NRF_EVT_RADIO_BLOCKED event, see @ref NRF_SOC_EVTS. + * @note The jitter in the start time of the radio timeslots is +/- @ref NRF_RADIO_START_JITTER_US us. + * @note The nrf_radio_signal_callback_t(@ref NRF_RADIO_CALLBACK_SIGNAL_TYPE_START) call has a latency relative to the + * specified radio timeslot start, but this does not affect the actual start time of the timeslot. + * @note NRF_TIMER0 is reset at the start of the radio timeslot, and is clocked at 1MHz from the high frequency + * (16 MHz) clock source. If p_request->hfclk_force_xtal is true, the high frequency clock is + * guaranteed to be clocked from the external crystal. + * @note The SoftDevice will neither access the NRF_RADIO peripheral nor the NRF_TIMER0 peripheral + * during the radio timeslot. + * + * @param[in] p_request Pointer to the request parameters. + * + * @retval ::NRF_ERROR_FORBIDDEN Either: + * - The session is not open. + * - The session is not IDLE. + * - This is the first request and its type is not @ref NRF_RADIO_REQ_TYPE_EARLIEST. + * - The request type was set to @ref NRF_RADIO_REQ_TYPE_NORMAL after a + * @ref NRF_RADIO_REQ_TYPE_EARLIEST request was blocked. + * @retval ::NRF_ERROR_INVALID_ADDR If the p_request pointer is invalid. + * @retval ::NRF_ERROR_INVALID_PARAM If the parameters of p_request are not valid. + * @retval ::NRF_SUCCESS Otherwise. + */ +SVCALL(SD_RADIO_REQUEST, uint32_t, sd_radio_request(nrf_radio_request_t const *p_request)); + +/**@brief Write register protected by the SoftDevice + * + * This function writes to a register that is write-protected by the SoftDevice. Please refer to your + * SoftDevice Specification for more details about which registers that are protected by SoftDevice. + * This function can write to the following protected peripheral: + * - ACL + * + * @note Protected registers may be read directly. + * @note Register that are write-once will return @ref NRF_SUCCESS on second set, even the value in + * the register has not changed. See the Product Specification for more details about register + * properties. + * + * @param[in] p_register Pointer to register to be written. + * @param[in] value Value to be written to the register. + * + * @retval ::NRF_ERROR_INVALID_ADDR This function can not write to the reguested register. + * @retval ::NRF_SUCCESS Value successfully written to register. + * + */ +SVCALL(SD_PROTECTED_REGISTER_WRITE, uint32_t, sd_protected_register_write(volatile uint32_t *p_register, uint32_t value)); + +/**@} */ + +#ifdef __cplusplus +} +#endif +#endif // NRF_SOC_H__ + +/**@} */ diff --git a/src/platform/nrf52/softdevice/nrf_svc.h b/src/platform/nrf52/softdevice/nrf_svc.h new file mode 100644 index 0000000..1de4465 --- /dev/null +++ b/src/platform/nrf52/softdevice/nrf_svc.h @@ -0,0 +1,98 @@ +/* + * Copyright (c) Nordic Semiconductor ASA + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form, except as embedded into a Nordic + * Semiconductor ASA integrated circuit in a product or a software update for + * such product, must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of Nordic Semiconductor ASA nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * 4. This software, with or without modification, must only be used with a + * Nordic Semiconductor ASA integrated circuit. + * + * 5. Any software provided in binary form under this license must not be reverse + * engineered, decompiled, modified and/or disassembled. + * + * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef NRF_SVC__ +#define NRF_SVC__ + +#include "stdint.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief Supervisor call declaration. + * + * A call to a function marked with @ref SVCALL, will trigger a Supervisor Call (SVC) Exception. + * The SVCs with SVC numbers 0x00-0x0F are forwared to the application. All other SVCs are handled by the SoftDevice. + * + * @param[in] number The SVC number to be used. + * @param[in] return_type The return type of the SVC function. + * @param[in] signature Function signature. The function can have at most four arguments. + */ + +#ifdef SVCALL_AS_NORMAL_FUNCTION +#define SVCALL(number, return_type, signature) return_type signature +#else + +#ifndef SVCALL +#if defined(__CC_ARM) +#define SVCALL(number, return_type, signature) return_type __svc(number) signature +#elif defined(__GNUC__) +#ifdef __cplusplus +#define GCC_CAST_CPP (uint16_t) +#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") + +#elif defined(__ICCARM__) +#define PRAGMA(x) _Pragma(#x) +#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 +#endif // SVCALL + +#endif // SVCALL_AS_NORMAL_FUNCTION + +#ifdef __cplusplus +} +#endif +#endif // NRF_SVC__ diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp new file mode 100644 index 0000000..78562e4 --- /dev/null +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -0,0 +1,420 @@ +#include "CryptoEngine.h" +#include "PortduinoGPIO.h" +#include "SPIChip.h" +#include "mesh/RF95Interface.h" +#include "sleep.h" +#include "target_specific.h" + +#include +#include + +#include "PortduinoGlue.h" +#include "linux/gpio/LinuxGPIOPin.h" +#include "yaml-cpp/yaml.h" +#include +#include +#include +#include + +std::map settingsMap; +std::map settingsStrings; +std::ofstream traceFile; +char *configPath = nullptr; + +// FIXME - move setBluetoothEnable into a HALPlatform class +void setBluetoothEnable(bool enable) +{ + // not needed +} + +void cpuDeepSleep(uint32_t msecs) +{ + notImplemented("cpuDeepSleep"); +} + +void updateBatteryLevel(uint8_t level) NOT_IMPLEMENTED("updateBatteryLevel"); + +int TCPPort = 4403; + +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 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."}, + {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); +} + +/** 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() +{ + printf("Setting up Meshtastic on Portduino...\n"); + int max_GPIO = 0; + const configNames GPIO_lines[] = {cs, + irq, + busy, + reset, + sx126x_ant_sw, + txen, + rxen, + displayDC, + displayCS, + displayBacklight, + displayBacklightPWMChannel, + displayReset, + touchscreenCS, + touchscreenIRQ, + user}; + + std::string gpioChipName = "gpiochip"; + settingsStrings[i2cdev] = ""; + settingsStrings[keyboardDevice] = ""; + settingsStrings[webserverrootpath] = ""; + settingsStrings[spidev] = ""; + settingsStrings[displayspidev] = ""; + settingsMap[spiSpeed] = 2000000; + settingsMap[ascii_logs] = !isatty(1); + settingsMap[displayPanel] = no_screen; + settingsMap[touchscreenModule] = no_touchscreen; + + YAML::Node yamlConfig; + + if (configPath != nullptr) { + if (loadConfig(configPath)) { + 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 && loadConfig("config.yaml")) { + std::cout << "Using local config.yaml as config file" << std::endl; + } else if (access("/etc/meshtasticd/config.yaml", R_OK) == 0 && loadConfig("/etc/meshtasticd/config.yaml")) { + std::cout << "Using /etc/meshtasticd/config.yaml as config file" << std::endl; + } else { + std::cout << "No 'config.yaml' found, running simulated." << std::endl; + settingsMap[maxnodes] = 200; // Default to 200 nodes + settingsMap[logoutputlevel] = level_debug; // Default to debug + // Set the random seed equal to TCPPort to have a different seed per instance + randomSeed(TCPPort); + return; + } + + if (settingsStrings[config_directory] != "") { + std::string filetype = ".yaml"; + for (const std::filesystem::directory_entry &entry : + std::filesystem::directory_iterator{settingsStrings[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()); + } + } + } + + // Rather important to set this, if not running simulated. + randomSeed(time(NULL)); + + gpioChipName += std::to_string(settingsMap[gpiochip]); + + for (configNames i : GPIO_lines) { + if (settingsMap.count(i) && settingsMap[i] > max_GPIO) + max_GPIO = settingsMap[i]; + } + + 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: Can we do this in the for loop above? + // TODO: If one of these fails, we should log and terminate + if (settingsMap.count(cs) > 0 && settingsMap[cs] != RADIOLIB_NC) { + if (initGPIOPin(settingsMap[cs], gpioChipName) != ERRNO_OK) { + settingsMap[cs] = RADIOLIB_NC; + } + } + if (settingsMap.count(irq) > 0 && settingsMap[irq] != RADIOLIB_NC) { + if (initGPIOPin(settingsMap[irq], gpioChipName) != ERRNO_OK) { + settingsMap[irq] = RADIOLIB_NC; + } + } + if (settingsMap.count(busy) > 0 && settingsMap[busy] != RADIOLIB_NC) { + if (initGPIOPin(settingsMap[busy], gpioChipName) != ERRNO_OK) { + settingsMap[busy] = RADIOLIB_NC; + } + } + if (settingsMap.count(reset) > 0 && settingsMap[reset] != RADIOLIB_NC) { + if (initGPIOPin(settingsMap[reset], gpioChipName) != ERRNO_OK) { + settingsMap[reset] = RADIOLIB_NC; + } + } + if (settingsMap.count(sx126x_ant_sw) > 0 && settingsMap[sx126x_ant_sw] != RADIOLIB_NC) { + if (initGPIOPin(settingsMap[sx126x_ant_sw], gpioChipName) != ERRNO_OK) { + settingsMap[sx126x_ant_sw] = RADIOLIB_NC; + } + } + if (settingsMap.count(user) > 0 && settingsMap[user] != RADIOLIB_NC) { + if (initGPIOPin(settingsMap[user], gpioChipName) != ERRNO_OK) { + settingsMap[user] = RADIOLIB_NC; + } + } + if (settingsMap.count(rxen) > 0 && settingsMap[rxen] != RADIOLIB_NC) { + if (initGPIOPin(settingsMap[rxen], gpioChipName) != ERRNO_OK) { + settingsMap[rxen] = RADIOLIB_NC; + } + } + if (settingsMap.count(txen) > 0 && settingsMap[txen] != RADIOLIB_NC) { + if (initGPIOPin(settingsMap[txen], gpioChipName) != ERRNO_OK) { + settingsMap[txen] = RADIOLIB_NC; + } + } + + if (settingsMap[displayPanel] != no_screen) { + if (settingsMap[displayCS] > 0) + initGPIOPin(settingsMap[displayCS], gpioChipName); + if (settingsMap[displayDC] > 0) + initGPIOPin(settingsMap[displayDC], gpioChipName); + if (settingsMap[displayBacklight] > 0) + initGPIOPin(settingsMap[displayBacklight], gpioChipName); + if (settingsMap[displayReset] > 0) + initGPIOPin(settingsMap[displayReset], gpioChipName); + } + if (settingsMap[touchscreenModule] != no_touchscreen) { + if (settingsMap[touchscreenCS] > 0) + initGPIOPin(settingsMap[touchscreenCS], gpioChipName); + if (settingsMap[touchscreenIRQ] > 0) + initGPIOPin(settingsMap[touchscreenIRQ], gpioChipName); + } + + if (settingsStrings[spidev] != "") { + SPI.begin(settingsStrings[spidev].c_str()); + } + if (settingsStrings[traceFilename] != "") { + try { + traceFile.open(settingsStrings[traceFilename], std::ios::out | std::ios::app); + } catch (std::ofstream::failure &e) { + std::cout << "*** traceFile Exception " << e.what() << std::endl; + exit(EXIT_FAILURE); + } + } + + return; +} + +int initGPIOPin(int pinNum, const std::string gpioChipName) +{ +#ifdef PORTDUINO_LINUX_HARDWARE + std::string gpio_name = "GPIO" + std::to_string(pinNum); + try { + GPIOPin *csPin; + csPin = new LinuxGPIOPin(pinNum, gpioChipName.c_str(), pinNum, gpio_name.c_str()); + csPin->setSilent(); + gpioBind(csPin); + return ERRNO_OK; + } catch (...) { + std::exception_ptr p = std::current_exception(); + std::cout << "Warning, cannot claim pin " << gpio_name << (p ? p.__cxa_exception_type()->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") { + settingsMap[logoutputlevel] = level_trace; + } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "debug") { + settingsMap[logoutputlevel] = level_debug; + } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "info") { + settingsMap[logoutputlevel] = level_info; + } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "warn") { + settingsMap[logoutputlevel] = level_warn; + } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "error") { + settingsMap[logoutputlevel] = level_error; + } + settingsStrings[traceFilename] = yamlConfig["Logging"]["TraceFile"].as(""); + if (yamlConfig["Logging"]["AsciiLogs"]) { + // Default is !isatty(1) but can be set explicitly in config.yaml + settingsMap[ascii_logs] = yamlConfig["Logging"]["AsciiLogs"].as(); + } + } + if (yamlConfig["Lora"]) { + settingsMap[use_sx1262] = false; + settingsMap[use_rf95] = false; + settingsMap[use_sx1280] = false; + settingsMap[use_sx1268] = false; + + if (yamlConfig["Lora"]["Module"] && yamlConfig["Lora"]["Module"].as("") == "sx1262") { + settingsMap[use_sx1262] = true; + } else if (yamlConfig["Lora"]["Module"] && yamlConfig["Lora"]["Module"].as("") == "RF95") { + settingsMap[use_rf95] = true; + } else if (yamlConfig["Lora"]["Module"] && yamlConfig["Lora"]["Module"].as("") == "sx1280") { + settingsMap[use_sx1280] = true; + } else if (yamlConfig["Lora"]["Module"] && yamlConfig["Lora"]["Module"].as("") == "sx1268") { + settingsMap[use_sx1268] = true; + } + settingsMap[dio2_as_rf_switch] = yamlConfig["Lora"]["DIO2_AS_RF_SWITCH"].as(false); + settingsMap[dio3_tcxo_voltage] = yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as(false); + settingsMap[cs] = yamlConfig["Lora"]["CS"].as(RADIOLIB_NC); + settingsMap[irq] = yamlConfig["Lora"]["IRQ"].as(RADIOLIB_NC); + settingsMap[busy] = yamlConfig["Lora"]["Busy"].as(RADIOLIB_NC); + settingsMap[reset] = yamlConfig["Lora"]["Reset"].as(RADIOLIB_NC); + settingsMap[txen] = yamlConfig["Lora"]["TXen"].as(RADIOLIB_NC); + settingsMap[rxen] = yamlConfig["Lora"]["RXen"].as(RADIOLIB_NC); + settingsMap[sx126x_ant_sw] = yamlConfig["Lora"]["SX126X_ANT_SW"].as(RADIOLIB_NC); + settingsMap[gpiochip] = yamlConfig["Lora"]["gpiochip"].as(0); + settingsMap[ch341Quirk] = yamlConfig["Lora"]["ch341_quirk"].as(false); + settingsMap[spiSpeed] = yamlConfig["Lora"]["spiSpeed"].as(2000000); + + settingsStrings[spidev] = "/dev/" + yamlConfig["Lora"]["spidev"].as("spidev0.0"); + if (settingsStrings[spidev].length() == 14) { + int x = settingsStrings[spidev].at(11) - '0'; + int y = settingsStrings[spidev].at(13) - '0'; + if (x >= 0 && x < 10 && y >= 0 && y < 10) { + settingsMap[spidev] = x + y << 4; + settingsMap[displayspidev] = settingsMap[spidev]; + settingsMap[touchscreenspidev] = settingsMap[spidev]; + } + } + } + if (yamlConfig["GPIO"]) { + settingsMap[user] = yamlConfig["GPIO"]["User"].as(RADIOLIB_NC); + } + if (yamlConfig["GPS"]) { + std::string serialPath = yamlConfig["GPS"]["SerialPath"].as(""); + if (serialPath != "") { + Serial1.setPath(serialPath); + settingsMap[has_gps] = 1; + } + } + if (yamlConfig["I2C"]) { + settingsStrings[i2cdev] = yamlConfig["I2C"]["I2CDevice"].as(""); + } + if (yamlConfig["Display"]) { + if (yamlConfig["Display"]["Panel"].as("") == "ST7789") + settingsMap[displayPanel] = st7789; + else if (yamlConfig["Display"]["Panel"].as("") == "ST7735") + settingsMap[displayPanel] = st7735; + else if (yamlConfig["Display"]["Panel"].as("") == "ST7735S") + settingsMap[displayPanel] = st7735s; + else if (yamlConfig["Display"]["Panel"].as("") == "ST7796") + settingsMap[displayPanel] = st7796; + else if (yamlConfig["Display"]["Panel"].as("") == "ILI9341") + settingsMap[displayPanel] = ili9341; + else if (yamlConfig["Display"]["Panel"].as("") == "ILI9342") + settingsMap[displayPanel] = ili9342; + else if (yamlConfig["Display"]["Panel"].as("") == "ILI9488") + settingsMap[displayPanel] = ili9488; + else if (yamlConfig["Display"]["Panel"].as("") == "HX8357D") + settingsMap[displayPanel] = hx8357d; + else if (yamlConfig["Display"]["Panel"].as("") == "X11") + settingsMap[displayPanel] = x11; + settingsMap[displayHeight] = yamlConfig["Display"]["Height"].as(0); + settingsMap[displayWidth] = yamlConfig["Display"]["Width"].as(0); + settingsMap[displayDC] = yamlConfig["Display"]["DC"].as(-1); + settingsMap[displayCS] = yamlConfig["Display"]["CS"].as(-1); + settingsMap[displayRGBOrder] = yamlConfig["Display"]["RGBOrder"].as(false); + settingsMap[displayBacklight] = yamlConfig["Display"]["Backlight"].as(-1); + settingsMap[displayBacklightInvert] = yamlConfig["Display"]["BacklightInvert"].as(false); + settingsMap[displayBacklightPWMChannel] = yamlConfig["Display"]["BacklightPWMChannel"].as(-1); + settingsMap[displayReset] = yamlConfig["Display"]["Reset"].as(-1); + settingsMap[displayOffsetX] = yamlConfig["Display"]["OffsetX"].as(0); + settingsMap[displayOffsetY] = yamlConfig["Display"]["OffsetY"].as(0); + settingsMap[displayRotate] = yamlConfig["Display"]["Rotate"].as(false); + settingsMap[displayOffsetRotate] = yamlConfig["Display"]["OffsetRotate"].as(1); + settingsMap[displayInvert] = yamlConfig["Display"]["Invert"].as(false); + settingsMap[displayBusFrequency] = yamlConfig["Display"]["BusFrequency"].as(40000000); + if (yamlConfig["Display"]["spidev"]) { + settingsStrings[displayspidev] = "/dev/" + yamlConfig["Display"]["spidev"].as("spidev0.1"); + if (settingsStrings[displayspidev].length() == 14) { + int x = settingsStrings[displayspidev].at(11) - '0'; + int y = settingsStrings[displayspidev].at(13) - '0'; + if (x >= 0 && x < 10 && y >= 0 && y < 10) { + settingsMap[displayspidev] = x + y << 4; + settingsMap[touchscreenspidev] = settingsMap[displayspidev]; + } + } + } + } + if (yamlConfig["Touchscreen"]) { + if (yamlConfig["Touchscreen"]["Module"].as("") == "XPT2046") + settingsMap[touchscreenModule] = xpt2046; + else if (yamlConfig["Touchscreen"]["Module"].as("") == "STMPE610") + settingsMap[touchscreenModule] = stmpe610; + else if (yamlConfig["Touchscreen"]["Module"].as("") == "GT911") + settingsMap[touchscreenModule] = gt911; + else if (yamlConfig["Touchscreen"]["Module"].as("") == "FT5x06") + settingsMap[touchscreenModule] = ft5x06; + settingsMap[touchscreenCS] = yamlConfig["Touchscreen"]["CS"].as(-1); + settingsMap[touchscreenIRQ] = yamlConfig["Touchscreen"]["IRQ"].as(-1); + settingsMap[touchscreenBusFrequency] = yamlConfig["Touchscreen"]["BusFrequency"].as(1000000); + settingsMap[touchscreenRotate] = yamlConfig["Touchscreen"]["Rotate"].as(-1); + settingsMap[touchscreenI2CAddr] = yamlConfig["Touchscreen"]["I2CAddr"].as(-1); + if (yamlConfig["Touchscreen"]["spidev"]) { + settingsStrings[touchscreenspidev] = "/dev/" + yamlConfig["Touchscreen"]["spidev"].as(""); + if (settingsStrings[touchscreenspidev].length() == 14) { + int x = settingsStrings[touchscreenspidev].at(11) - '0'; + int y = settingsStrings[touchscreenspidev].at(13) - '0'; + if (x >= 0 && x < 10 && y >= 0 && y < 10) { + settingsMap[touchscreenspidev] = x + y << 4; + } + } + } + } + if (yamlConfig["Input"]) { + settingsStrings[keyboardDevice] = (yamlConfig["Input"]["KeyboardDevice"]).as(""); + } + + if (yamlConfig["Webserver"]) { + settingsMap[webserverport] = (yamlConfig["Webserver"]["Port"]).as(-1); + settingsStrings[webserverrootpath] = (yamlConfig["Webserver"]["RootPath"]).as(""); + } + + if (yamlConfig["General"]) { + settingsMap[maxnodes] = (yamlConfig["General"]["MaxNodes"]).as(200); + settingsMap[maxtophone] = (yamlConfig["General"]["MaxMessageQueue"]).as(100); + settingsStrings[config_directory] = (yamlConfig["General"]["ConfigDirectory"]).as(""); + } + + } 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; +} \ No newline at end of file diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h new file mode 100644 index 0000000..95d82c1 --- /dev/null +++ b/src/platform/portduino/PortduinoGlue.h @@ -0,0 +1,70 @@ +#pragma once +#include +#include + +enum configNames { + use_sx1262, + cs, + irq, + busy, + reset, + sx126x_ant_sw, + txen, + rxen, + dio2_as_rf_switch, + dio3_tcxo_voltage, + ch341Quirk, + use_rf95, + use_sx1280, + use_sx1268, + user, + gpiochip, + spidev, + spiSpeed, + i2cdev, + has_gps, + touchscreenModule, + touchscreenCS, + touchscreenIRQ, + touchscreenI2CAddr, + touchscreenBusFrequency, + touchscreenRotate, + touchscreenspidev, + displayspidev, + displayBusFrequency, + displayPanel, + displayWidth, + displayHeight, + displayCS, + displayDC, + displayRGBOrder, + displayBacklight, + displayBacklightPWMChannel, + displayBacklightInvert, + displayReset, + displayRotate, + displayOffsetRotate, + displayOffsetX, + displayOffsetY, + displayInvert, + keyboardDevice, + logoutputlevel, + traceFilename, + webserver, + webserverport, + webserverrootpath, + maxtophone, + maxnodes, + ascii_logs, + config_directory +}; +enum { no_screen, x11, st7789, st7735, st7735s, st7796, ili9341, ili9342, ili9488, hx8357d }; +enum { no_touchscreen, xpt2046, stmpe610, gt911, ft5x06 }; +enum { level_error, level_warn, level_info, level_debug, level_trace }; + +extern std::map settingsMap; +extern std::map settingsStrings; +extern std::ofstream traceFile; +int initGPIOPin(int pinNum, std::string gpioChipname); +bool loadConfig(const char *configPath); +static bool ends_with(std::string_view str, std::string_view suffix); \ No newline at end of file diff --git a/src/platform/portduino/SimRadio.cpp b/src/platform/portduino/SimRadio.cpp new file mode 100644 index 0000000..840a0f5 --- /dev/null +++ b/src/platform/portduino/SimRadio.cpp @@ -0,0 +1,268 @@ +#include "SimRadio.h" +#include "MeshService.h" +#include "Router.h" + +SimRadio::SimRadio() : NotifiedWorkerThread("SimRadio") +{ + instance = this; +} + +SimRadio *SimRadio::instance; + +ErrorCode SimRadio::send(meshtastic_MeshPacket *p) +{ + printPacket("enqueuing for send", p); + + ErrorCode res = txQueue.enqueue(p) ? ERRNO_OK : ERRNO_UNKNOWN; + + 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 transmitting."); + 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. + + /* 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); + startTransmitTimerSNR(p->rx_snr); + } +} + +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::startTransmitTimerSNR(float snr) +{ + // If we have work to do and the timer wasn't already scheduled, schedule it now + if (!txQueue.empty()) { + uint32_t delayMsec = getTxDelayMsecWeighted(snr); + // 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::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++; + printPacket("Completed sending", p); + + // 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(); + + 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 false; // TODO check how this should be simulated +} + +bool SimRadio::isChannelActive() +{ + return false; // TODO ask simulator +} + +/** 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 result = (p != NULL); + LOG_DEBUG("cancelSending id=0x%x, removed=%d", id, result); + return result; +} + +void SimRadio::onNotify(uint32_t notification) +{ + switch (notification) { + case ISR_TX: + handleTransmitInterrupt(); + // LOG_DEBUG("tx complete - starting timer"); + startTransmitTimer(); + break; + case ISR_RX: + // LOG_DEBUG("rx complete - starting timer"); + startTransmitTimer(); + break; + case TRANSMIT_DELAY_COMPLETED: + 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 = 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("Starting low level send", txp); + 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("Sending 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 is larger than compressed message allows! Sending 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 +} + +void SimRadio::startReceive(meshtastic_MeshPacket *p) +{ + isReceiving = true; + size_t length = getPacketLength(p); + uint32_t xmitMsec = getPacketTime(length); + delay(xmitMsec); // Model the time it is busy receiving + handleReceiveInterrupt(p); +} + +meshtastic_QueueStatus SimRadio::getQueueStatus() +{ + meshtastic_QueueStatus qs; + + qs.res = qs.mesh_packet_id = 0; + qs.free = txQueue.getFree(); + qs.maxlen = txQueue.getMaxLen(); + + return qs; +} + +void SimRadio::handleReceiveInterrupt(meshtastic_MeshPacket *p) +{ + LOG_DEBUG("HANDLE RECEIVE INTERRUPT"); + uint32_t xmitMsec; + + if (!isReceiving) { + LOG_DEBUG("*** WAS_ASSERT *** handleReceiveInterrupt called when not in receive mode"); + return; + } + + isReceiving = false; + + // read the number of actually received bytes + size_t length = getPacketLength(p); + xmitMsec = getPacketTime(length); + // LOG_DEBUG("Payload size %d vs length (includes header) %d", p->decoded.payload.size, length); + + meshtastic_MeshPacket *mp = packetPool.allocCopy(*p); // keep a copy in packetPool + + printPacket("Lora RX", mp); + + airTime->logAirtime(RX_LOG, xmitMsec); + + deliverToReceiver(mp); +} + +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; + + if (state == RADIOLIB_ERR_NONE) { + // add null terminator + data[len] = 0; + } + + return state; +} \ No newline at end of file diff --git a/src/platform/portduino/SimRadio.h b/src/platform/portduino/SimRadio.h new file mode 100644 index 0000000..1edb496 --- /dev/null +++ b/src/platform/portduino/SimRadio.h @@ -0,0 +1,85 @@ +#pragma once + +#include "MeshPacketQueue.h" +#include "RadioInterface.h" +#include "api/WiFiServerAPI.h" +#include "concurrency/NotifiedWorkerThread.h" + +#include + +class SimRadio : public RadioInterface, protected concurrency::NotifiedWorkerThread +{ + enum PendingISR { ISR_NONE = 0, ISR_RX, ISR_TX, TRANSMIT_DELAY_COMPLETED }; + + /** + * Debugging counts + */ + uint32_t rxBad = 0, rxGood = 0, txGood = 0; + + MeshPacketQueue txQueue = MeshPacketQueue(MAX_TX_QUEUE); + + public: + SimRadio(); + + /** MeshService needs this to find our active instance + */ + static SimRadio *instance; + + virtual ErrorCode send(meshtastic_MeshPacket *p) override; + + /** 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(); + + /** 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; + + /** + * 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; + + protected: + /// are _trying_ to receive a packet currently (note - we might just be waiting for one) + bool isReceiving = false; + + private: + void setTransmitDelay(); + + /** random timer with certain min. and max. settings */ + void startTransmitTimer(bool withDelay = true); + + /** timer scaled to SNR of to be flooded packet */ + void startTransmitTimerSNR(float snr); + + void handleTransmitInterrupt(); + void handleReceiveInterrupt(meshtastic_MeshPacket *p); + + void onNotify(uint32_t notification); + + // start an immediate transmit + virtual void startSend(meshtastic_MeshPacket *txp); + + // derive packet length + size_t getPacketLength(meshtastic_MeshPacket *p); + + int16_t readData(uint8_t *str, size_t len); + + 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(); +}; + +extern SimRadio *simRadio; \ No newline at end of file diff --git a/src/platform/portduino/architecture.h b/src/platform/portduino/architecture.h new file mode 100644 index 0000000..3219492 --- /dev/null +++ b/src/platform/portduino/architecture.h @@ -0,0 +1,19 @@ +#pragma once + +#define ARCH_PORTDUINO 1 + +// +// set HW_VENDOR +// + +#define HW_VENDOR meshtastic_HardwareModel_PORTDUINO + +#ifndef HAS_WIFI +#define HAS_WIFI 1 +#endif +#ifndef HAS_RTC +#define HAS_RTC 1 +#endif +#ifndef HAS_TELEMETRY +#define HAS_TELEMETRY 1 +#endif \ No newline at end of file diff --git a/src/platform/rp2xx0/architecture.h b/src/platform/rp2xx0/architecture.h new file mode 100644 index 0000000..8c7dfc0 --- /dev/null +++ b/src/platform/rp2xx0/architecture.h @@ -0,0 +1,36 @@ +#pragma once + +#define ARCH_RP2040 + +#ifndef HAS_BUTTON +#define HAS_BUTTON 1 +#endif +#ifndef HAS_TELEMETRY +#define HAS_TELEMETRY 1 +#endif +#ifndef HAS_SCREEN +#define HAS_SCREEN 1 +#endif +#ifndef HAS_WIRE +#define HAS_WIRE 1 +#endif +#ifndef HAS_SENSOR +#define HAS_SENSOR 1 +#endif +#ifndef HAS_RADIO +#define HAS_RADIO 1 +#endif + +#if defined(RPI_PICO) +#define HW_VENDOR meshtastic_HardwareModel_RPI_PICO +#elif defined(RPI_PICO2) +#define HW_VENDOR meshtastic_HardwareModel_RPI_PICO2 +#elif defined(RAK11310) +#define HW_VENDOR meshtastic_HardwareModel_RAK11310 +#elif defined(SENSELORA_RP2040) +#define HW_VENDOR meshtastic_HardwareModel_SENSELORA_RP2040 +#elif defined(RP2040_LORA) +#define HW_VENDOR meshtastic_HardwareModel_RP2040_LORA +#elif defined(RP2040_FEATHER_RFM95) +#define HW_VENDOR meshtastic_HardwareModel_RP2040_FEATHER_RFM95 +#endif \ No newline at end of file diff --git a/src/platform/rp2xx0/hardware_rosc/include/hardware/rosc.h b/src/platform/rp2xx0/hardware_rosc/include/hardware/rosc.h new file mode 100644 index 0000000..e1e014f --- /dev/null +++ b/src/platform/rp2xx0/hardware_rosc/include/hardware/rosc.h @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef _HARDWARE_ROSC_H_ +#define _HARDWARE_ROSC_H_ + +#include "hardware/structs/rosc.h" +#include "pico.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** \file rosc.h + * \defgroup hardware_rosc hardware_rosc + * + * 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. + */ + +/*! \brief Set frequency of the Ring Oscillator + * \ingroup hardware_rosc + * + * \param code The drive strengths. See the RP2040 datasheet for information on this value. + */ +void rosc_set_freq(uint32_t code); + +/*! \brief Set range of the Ring Oscillator + * \ingroup hardware_rosc + * + * Frequency range. Frequencies will vary with Process, Voltage & Temperature (PVT). + * Clock output will not glitch when changing the range up one step at a time. + * + * \param range 0x01 Low, 0x02 Medium, 0x03 High, 0x04 Too High. + */ +void rosc_set_range(uint range); + +/*! \brief Disable the Ring Oscillator + * \ingroup hardware_rosc + * + */ +void rosc_disable(void); + +/*! \brief Put Ring Oscillator in to dormant mode. + * \ingroup hardware_rosc + * + * The ROSC supports a dormant mode,which stops oscillation until woken up up by an asynchronous interrupt. + * This can either come from the RTC, being clocked by an external clock, or a GPIO pin going high or low. + * If no IRQ is configured before going into dormant mode the ROSC will never restart. + * + * PLLs should be stopped before selecting dormant mode. + */ +void rosc_set_dormant(void); + +// FIXME: Add doxygen + +uint32_t next_rosc_code(uint32_t code); + +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 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()); +}; + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/src/platform/rp2xx0/hardware_rosc/rosc.c b/src/platform/rp2xx0/hardware_rosc/rosc.c new file mode 100644 index 0000000..f79929f --- /dev/null +++ b/src/platform/rp2xx0/hardware_rosc/rosc.c @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "pico.h" + +// For MHZ definitions etc +#include "hardware/clocks.h" +#include "hardware/rosc.h" + +// 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; +} + +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; +} + +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_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_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 new file mode 100644 index 0000000..a46b0fa --- /dev/null +++ b/src/platform/rp2xx0/main-rp2xx0.cpp @@ -0,0 +1,147 @@ +#include "configuration.h" +#include "hardware/xosc.h" +#include +#include +#include +#include +#include + +void setBluetoothEnable(bool enable) +{ + // not needed +} + +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; +} + +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) +{ + + 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); + + // 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(); + + /* Set RP2040 in dormant mode. Will not wake up. */ + // xosc_dormant(); +} + +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 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); + + 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); +} + +/* 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); +} +#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 new file mode 100644 index 0000000..17dff24 --- /dev/null +++ b/src/platform/rp2xx0/pico_sleep/include/pico/sleep.h @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef _PICO_SLEEP_H_ +#define _PICO_SLEEP_H_ + +#include "hardware/rtc.h" +#include "pico.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** \file sleep.h + * \defgroup hardware_sleep hardware_sleep + * + * Lower Power Sleep API + * + * The difference between sleep and dormant is that ALL clocks are stopped in dormant mode, + * until the source (either xosc or rosc) is started again by an external event. + * In sleep mode some clocks can be left running controlled by the SLEEP_EN registers in the clocks + * block. For example you could keep clk_rtc running. Some destinations (proc0 and proc1 wakeup logic) + * can't be stopped in sleep mode otherwise there wouldn't be enough logic to wake up again. + * + * \subsection sleep_example Example + * \addtogroup hardware_sleep + * \include hello_sleep.c + + */ +typedef enum { DORMANT_SOURCE_NONE, DORMANT_SOURCE_XOSC, DORMANT_SOURCE_ROSC } dormant_source_t; + +/*! \brief Set all clock sources to the the dormant clock source to prepare for sleep. + * \ingroup hardware_sleep + * + * \param dormant_source The dormant clock source to use + */ +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); +} + +/*! \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); +} + +/*! \brief Send system to sleep until the specified time + * \ingroup hardware_sleep + * + * One of the sleep_run_* functions must be called prior to this call + * + * \param t The time to wake up + * \param callback Function to call on wakeup. + */ +void sleep_goto_sleep_until(datetime_t *t, rtc_callback_t callback); + +/*! \brief Send system to sleep until the specified GPIO changes + * \ingroup hardware_sleep + * + * One of the sleep_run_* functions must be called prior to this call + * + * \param gpio_pin The pin to provide the wake up + * \param edge true for leading edge, false for trailing edge + * \param high true for active high, false for active low + */ +void sleep_goto_dormant_until_pin(uint gpio_pin, bool edge, bool high); + +/*! \brief Send system to sleep until a leading high edge is detected on GPIO + * \ingroup hardware_sleep + * + * One of the sleep_run_* functions must be called prior to this call + * + * \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); +} + +/*! \brief Send system to sleep until a high level is detected on GPIO + * \ingroup hardware_sleep + * + * One of the sleep_run_* functions must be called prior to this call + * + * \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); +} + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/src/platform/rp2xx0/pico_sleep/sleep.c b/src/platform/rp2xx0/pico_sleep/sleep.c new file mode 100644 index 0000000..65096be --- /dev/null +++ b/src/platform/rp2xx0/pico_sleep/sleep.c @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "pico.h" + +#include "pico/sleep.h" +#include "pico/stdlib.h" + +#include "hardware/clocks.h" +#include "hardware/pll.h" +#include "hardware/regs/io_bank0.h" +#include "hardware/rosc.h" +#include "hardware/rtc.h" +#include "hardware/xosc.h" +// For __wfi +#include "hardware/sync.h" +// For scb_hw so we can enable deep sleep +#include "hardware/structs/scb.h" +// when using old SDK this macro is not defined +#ifndef XOSC_HZ +#define XOSC_HZ 12000000u +#endif +// The difference between sleep and dormant is that ALL clocks are stopped in dormant mode, +// until the source (either xosc or rosc) is started again by an external event. +// In sleep mode some clocks can be left running controlled by the SLEEP_EN registers in the clocks +// block. For example you could keep clk_rtc running. Some destinations (proc0 and proc1 wakeup logic) +// can't be stopped in sleep mode otherwise there wouldn't be enough logic to wake up again. + +// TODO: Optionally, memories can also be powered down. + +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); +} + +// 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; + + // 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 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 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; + + 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); + + 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(); + } + + // 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)); + + // 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); + + uint save = scb_hw->scr; + // Enable deep sleep at the proc + scb_hw->scr = save | M0PLUS_SCR_SLEEPDEEP_BITS; + + // Go to sleep + __wfi(); +} + +static void _go_dormant(void) +{ + assert(dormant_source_valid(_dormant_source)); + + 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; + + // Configure the appropriate IRQ at IO bank 0 + assert(gpio_pin < NUM_BANK0_GPIOS); + + 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; + + gpio_set_dormant_irq_enabled(gpio_pin, event, true); + + _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); +} \ No newline at end of file diff --git a/src/platform/stm32wl/architecture.h b/src/platform/stm32wl/architecture.h new file mode 100644 index 0000000..325a192 --- /dev/null +++ b/src/platform/stm32wl/architecture.h @@ -0,0 +1,31 @@ +#pragma once + +#define ARCH_STM32WL + +// +// defaults for STM32WL architecture +// + +#ifndef HAS_RADIO +#define HAS_RADIO 1 +#endif +#ifndef HAS_TELEMETRY +#define HAS_TELEMETRY 1 +#endif + +// +// set HW_VENDOR +// +#ifdef _VARIANT_WIOE5_ +#define HW_VENDOR meshtastic_HardwareModel_WIO_E5 +#elif defined(_VARIANT_RAK3172_) +#define HW_VENDOR meshtastic_HardwareModel_RAK3172 +#else +#define HW_VENDOR meshtastic_HardwareModel_PRIVATE_HW +#endif + +/* virtual pins */ +#define SX126X_CS 1000 +#define SX126X_DIO1 1001 +#define SX126X_RESET 1003 +#define SX126X_BUSY 1004 \ No newline at end of file diff --git a/src/platform/stm32wl/main-stm32wl.cpp b/src/platform/stm32wl/main-stm32wl.cpp new file mode 100644 index 0000000..3eddbb3 --- /dev/null +++ b/src/platform/stm32wl/main-stm32wl.cpp @@ -0,0 +1,28 @@ +#include "RTC.h" +#include "configuration.h" +#include +#include + +void setBluetoothEnable(bool enable) {} + +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) + + // 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) {} diff --git a/src/power.h b/src/power.h new file mode 100644 index 0000000..6333510 --- /dev/null +++ b/src/power.h @@ -0,0 +1,102 @@ +#pragma once +#include "../variants/rak2560/RAK9154Sensor.h" +#include "PowerStatus.h" +#include "concurrency/OSThread.h" +#include "configuration.h" + +#ifdef ARCH_ESP32 +// "legacy adc calibration driver is deprecated, please migrate to use esp_adc/adc_cali.h and esp_adc/adc_cali_scheme.h +#include +#include +#endif + +#ifndef NUM_OCV_POINTS +#define NUM_OCV_POINTS 11 +#endif + +#ifndef OCV_ARRAY +#ifdef CELL_TYPE_LIFEPO4 +#define OCV_ARRAY 3400, 3350, 3320, 3290, 3270, 3260, 3250, 3230, 3200, 3120, 3000 +#elif defined(CELL_TYPE_LEADACID) +#define OCV_ARRAY 2120, 2090, 2070, 2050, 2030, 2010, 1990, 1980, 1970, 1960, 1950 +#elif defined(CELL_TYPE_ALKALINE) +#define OCV_ARRAY 1580, 1400, 1350, 1300, 1280, 1250, 1230, 1190, 1150, 1100, 1000 +#elif defined(CELL_TYPE_NIMH) +#define OCV_ARRAY 1400, 1300, 1280, 1270, 1260, 1250, 1240, 1230, 1210, 1150, 1000 +#elif defined(CELL_TYPE_LTO) +#define OCV_ARRAY 2700, 2560, 2540, 2520, 2500, 2460, 2420, 2400, 2380, 2320, 1500 +#else // LiIon +#define OCV_ARRAY 4190, 4050, 3990, 3890, 3800, 3720, 3630, 3530, 3420, 3300, 3100 +#endif +#endif + +/*Note: 12V lead acid is 6 cells, most board accept only 1 cell LiIon/LiPo*/ +#ifndef NUM_CELLS +#define NUM_CELLS 1 +#endif + +#ifdef BAT_MEASURE_ADC_UNIT +extern RTC_NOINIT_ATTR uint64_t RTC_reg_b; +#include "soc/sens_reg.h" // needed for adc pin reset +#endif + +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_PORTDUINO) +#include "modules/Telemetry/Sensor/INA219Sensor.h" +#include "modules/Telemetry/Sensor/INA260Sensor.h" +#include "modules/Telemetry/Sensor/INA3221Sensor.h" +extern INA260Sensor ina260Sensor; +extern INA219Sensor ina219Sensor; +extern INA3221Sensor ina3221Sensor; +#endif + +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) +#include "modules/Telemetry/Sensor/MAX17048Sensor.h" +extern MAX17048Sensor max17048Sensor; +#endif + +#if HAS_RAKPROT && !defined(ARCH_PORTDUINO) +#include "../variants/rak2560/RAK9154Sensor.h" +extern RAK9154Sensor rak9154Sensor; +#endif + +#ifdef HAS_PMU +#include "XPowersAXP192.tpp" +#include "XPowersAXP2101.tpp" +#include "XPowersLibInterface.hpp" +extern XPowersLibInterface *PMU; +#endif + +class Power : private concurrency::OSThread +{ + + public: + Observable newStatus; + + Power(); + + void shutdown(); + 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; + + /// 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(); + + private: + // open circuit voltage lookup table + uint8_t low_voltage_counter; +#ifdef DEBUG_HEAP + uint32_t lastheap; +#endif +}; + +extern Power *power; \ No newline at end of file diff --git a/src/serialization/JSON.cpp b/src/serialization/JSON.cpp new file mode 100644 index 0000000..42e6151 --- /dev/null +++ b/src/serialization/JSON.cpp @@ -0,0 +1,245 @@ +/* + * File JSON.cpp part of the SimpleJSON Library - http://mjpa.in/json + * + * Copyright (C) 2010 Mike Anchor + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "JSON.h" + +/** + * Blocks off the public constructor + * + * @access private + * + */ +JSON::JSON() {} + +/** + * Parses a complete JSON encoded string + * + * @access public + * + * @param char* data The JSON text + * + * @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; + + // 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; + } + + // We're now at the end of the string + return value; +} + +/** + * Turns the passed in JSONValue into a JSON encode string + * + * @access public + * + * @param JSONValue* value The root value + * + * @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 ""; +} + +/** + * Skips over any whitespace characters (space, tab, \r or \n) defined by the JSON spec + * + * @access protected + * + * @param char** data Pointer to a char* that contains the JSON text + * + * @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)++; + + return **data != 0; +} + +/** + * Extracts a JSON String as defined by the spec - "" + * Any escaped characters are swapped out for their unescaped values + * + * @access protected + * + * @param char** data Pointer to a char* that contains the JSON text + * @param std::string& str Reference to a std::string to receive the extracted string + * + * @return bool Returns true on success, false on failure + */ +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; + + // 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 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; + + // 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 + return false; + } + + // Add the next char + str += next_char; + + // Move on + (*data)++; + } + + // If we're here, the string ended incorrectly + return false; +} + +/** + * Parses some text as though it is an integer + * + * @access protected + * + * @param char** data Pointer to a char* that contains the JSON text + * + * @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'); + + return integer; +} + +/** + * Parses some text as though it is a decimal + * + * @access protected + * + * @param char** data Pointer to a char* that contains the JSON text + * + * @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; +} diff --git a/src/serialization/JSON.h b/src/serialization/JSON.h new file mode 100644 index 0000000..33f34e6 --- /dev/null +++ b/src/serialization/JSON.h @@ -0,0 +1,73 @@ +/* + * File JSON.h part of the SimpleJSON Library - http://mjpa.in/json + * + * Copyright (C) 2010 Mike Anchor + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef _JSON_H_ +#define _JSON_H_ + +#include +#include +#include +#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; + + const char *save = s; + while (n-- > 0) { + if (*(save++) == 0) + return false; + } + + return true; +} + +// Custom types +class JSONValue; +typedef std::vector JSONArray; +typedef std::map JSONObject; + +#include "JSONValue.h" + +class JSON +{ + friend class JSONValue; + + 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); + + private: + JSON(); +}; + +#endif diff --git a/src/serialization/JSONValue.cpp b/src/serialization/JSONValue.cpp new file mode 100644 index 0000000..b2e9575 --- /dev/null +++ b/src/serialization/JSONValue.cpp @@ -0,0 +1,890 @@ +/* + * File JSONValue.cpp part of the SimpleJSON Library - http://mjpa.in/json + * + * Copyright (C) 2010 Mike Anchor + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#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; \ + } \ + } + +/** + * Parses a JSON encoded value to a JSONValue object + * + * @access protected + * + * @param char** data Pointer to a char* that contains the data + * + * @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; + } + + // 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; + + (*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; + } +} + +/** + * Basic constructor for creating a JSON Value of type NULL + * + * @access public + */ +JSONValue::JSONValue(/*NULL*/) +{ + type = JSONType_Null; +} + +/** + * Basic constructor for creating a JSON Value of type String + * + * @access public + * + * @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)); +} + +/** + * Basic constructor for creating a JSON Value of type String + * + * @access public + * + * @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); +} + +/** + * Basic constructor for creating a JSON Value of type Bool + * + * @access public + * + * @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; +} + +/** + * Basic constructor for creating a JSON Value of type Number + * + * @access public + * + * @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; +} + +/** + * Basic constructor for creating a JSON Value of type Number + * + * @access public + * + * @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; +} + +/** + * Basic constructor for creating a JSON Value of type Number + * + * @access public + * + * @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; +} + +/** + * Basic constructor for creating a JSON Value of type Array + * + * @access public + * + * @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); +} + +/** + * Basic constructor for creating a JSON Value of type Object + * + * @access public + * + * @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); +} + +/** + * Copy constructor to perform a deep copy of array / object values + * + * @access public + * + * @param JSONValue m_source The source JSONValue that is being copied + */ +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; + + case JSONType_Bool: + bool_value = m_source.bool_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)); + } + break; + } + + case JSONType_Null: + // Nothing to do. + break; + } +} + +/** + * The destructor for the JSON Value object + * Handles deleting the objects in the array or the object value + * + * @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; + } +} + +/** + * Checks if the value is a NULL + * + * @access public + * + * @return bool Returns true if it is a NULL value, false otherwise + */ +bool JSONValue::IsNull() const +{ + return type == JSONType_Null; +} + +/** + * Checks if the value is a String + * + * @access public + * + * @return bool Returns true if it is a String value, false otherwise + */ +bool JSONValue::IsString() const +{ + return type == JSONType_String; +} + +/** + * Checks if the value is a Bool + * + * @access public + * + * @return bool Returns true if it is a Bool value, false otherwise + */ +bool JSONValue::IsBool() const +{ + return type == JSONType_Bool; +} + +/** + * Checks if the value is a Number + * + * @access public + * + * @return bool Returns true if it is a Number value, false otherwise + */ +bool JSONValue::IsNumber() const +{ + return type == JSONType_Number; +} + +/** + * Checks if the value is an Array + * + * @access public + * + * @return bool Returns true if it is an Array value, false otherwise + */ +bool JSONValue::IsArray() const +{ + return type == JSONType_Array; +} + +/** + * Checks if the value is an Object + * + * @access public + * + * @return bool Returns true if it is an Object value, false otherwise + */ +bool JSONValue::IsObject() const +{ + return type == JSONType_Object; +} + +/** + * Retrieves the String value of this JSONValue + * Use IsString() before using this method. + * + * @access public + * + * @return std::string Returns the string value + */ +const std::string &JSONValue::AsString() const +{ + return (*string_value); +} + +/** + * Retrieves the Bool value of this JSONValue + * Use IsBool() before using this method. + * + * @access public + * + * @return bool Returns the bool value + */ +bool JSONValue::AsBool() const +{ + return bool_value; +} + +/** + * Retrieves the Number value of this JSONValue + * Use IsNumber() before using this method. + * + * @access public + * + * @return double Returns the number value + */ +double JSONValue::AsNumber() const +{ + return number_value; +} + +/** + * Retrieves the Array value of this JSONValue + * Use IsArray() before using this method. + * + * @access public + * + * @return JSONArray Returns the array value + */ +const JSONArray &JSONValue::AsArray() const +{ + return (*array_value); +} + +/** + * Retrieves the Object value of this JSONValue + * Use IsObject() before using this method. + * + * @access public + * + * @return JSONObject Returns the object value + */ +const JSONObject &JSONValue::AsObject() const +{ + return (*object_value); +} + +/** + * Retrieves the number of children of this JSONValue. + * This number will be 0 or the actual number of children + * if IsArray() or IsObject(). + * + * @access public + * + * @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; + } +} + +/** + * Checks if this JSONValue has a child at the given index. + * Use IsArray() before using this method. + * + * @access public + * + * @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; + } +} + +/** + * Retrieves the child of this JSONValue at the given index. + * Use IsArray() before using this method. + * + * @access public + * + * @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; + } +} + +/** + * Checks if this JSONValue has a child at the given key. + * Use IsObject() before using this method. + * + * @access public + * + * @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; + } +} + +/** + * Retrieves the child of this JSONValue at the given key. + * Use IsObject() before using this method. + * + * @access public + * + * @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; + } +} + +/** + * Retrieves the keys of the JSON Object or an empty vector + * if this value is not an object. + * + * @access public + * + * @return std::vector A vector containing the 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); + + ++iter; + } + } + + return keys; +} + +/** + * Creates a JSON encoded string for the value with all necessary characters escaped + * + * @access public + * + * @param bool prettyprint Enable prettyprint + * + * @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); +} + +/** + * Creates a JSON encoded string for the value with all necessary characters escaped + * + * @access private + * + * @param size_t indentDepth The prettyprint indentation depth (0 : no prettyprint) + * + * @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); + + switch (type) { + case JSONType_Null: + ret_string = "null"; + break; + + case JSONType_String: + ret_string = StringifyString(*string_value); + 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_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; + } + + 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; + } + } + + return ret_string; +} + +/** + * Creates a JSON encoded string with all required fields escaped + * Works from http://www.ecma-internationl.org/publications/files/ECMA-ST/ECMA-262.pdf + * Section 15.12.3. + * + * @access private + * + * @param std::string str The string that needs to have the characters escaped + * + * @return std::string Returns the JSON string + */ +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 < ' ' || chr > 126) { + str_out += "\\u"; + for (int i = 0; i < 4; i++) { + int value = (chr >> 12) & 0xf; + if (value >= 0 && value <= 9) + str_out += (char)('0' + value); + else if (value >= 10 && value <= 15) + str_out += (char)('A' + (value - 10)); + chr <<= 4; + } + } else { + str_out += chr; + } + + ++iter; + } + + str_out += "\""; + return str_out; +} + +/** + * Creates the indentation string for the depth given + * + * @access private + * + * @param size_t indent The prettyprint indentation depth (0 : no indentation) + * + * @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; +} \ No newline at end of file diff --git a/src/serialization/JSONValue.h b/src/serialization/JSONValue.h new file mode 100644 index 0000000..16d53e8 --- /dev/null +++ b/src/serialization/JSONValue.h @@ -0,0 +1,95 @@ +/* + * File JSONValue.h part of the SimpleJSON Library - http://mjpa.in/json + * + * Copyright (C) 2010 Mike Anchor + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef _JSONVALUE_H_ +#define _JSONVALUE_H_ + +#include +#include + +#include "JSON.h" + +class JSON; + +enum JSONType { JSONType_Null, JSONType_String, JSONType_Bool, JSONType_Number, JSONType_Array, JSONType_Object }; + +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(); + + 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; + + 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; + + 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); + + JSONType type; + + 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 new file mode 100644 index 0000000..21fb377 --- /dev/null +++ b/src/serialization/MeshPacketSerializer.cpp @@ -0,0 +1,358 @@ +#ifndef NRF52_USE_JSON +#include "MeshPacketSerializer.h" +#include "JSON.h" +#include "NodeDB.h" +#include "mesh/generated/meshtastic/mqtt.pb.h" +#include "mesh/generated/meshtastic/telemetry.pb.h" +#include "modules/RoutingModule.h" +#include +#include +#if defined(ARCH_ESP32) +#include "../mesh/generated/meshtastic/paxcount.pb.h" +#endif +#include "mesh/generated/meshtastic/remote_hardware.pb.h" + +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); + + 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"); + + 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) { + msgPayload["battery_level"] = new JSONValue((unsigned 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) { + msgPayload["temperature"] = new JSONValue(decoded->variant.environment_metrics.temperature); + msgPayload["relative_humidity"] = new JSONValue(decoded->variant.environment_metrics.relative_humidity); + msgPayload["barometric_pressure"] = new JSONValue(decoded->variant.environment_metrics.barometric_pressure); + msgPayload["gas_resistance"] = new JSONValue(decoded->variant.environment_metrics.gas_resistance); + msgPayload["voltage"] = new JSONValue(decoded->variant.environment_metrics.voltage); + msgPayload["current"] = new JSONValue(decoded->variant.environment_metrics.current); + msgPayload["lux"] = new JSONValue(decoded->variant.environment_metrics.lux); + msgPayload["white_lux"] = new JSONValue(decoded->variant.environment_metrics.white_lux); + msgPayload["iaq"] = new JSONValue((uint)decoded->variant.environment_metrics.iaq); + msgPayload["wind_speed"] = new JSONValue(decoded->variant.environment_metrics.wind_speed); + msgPayload["wind_direction"] = new JSONValue((uint)decoded->variant.environment_metrics.wind_direction); + msgPayload["wind_gust"] = new JSONValue(decoded->variant.environment_metrics.wind_gust); + msgPayload["wind_lull"] = new JSONValue(decoded->variant.environment_metrics.wind_lull); + } else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { + msgPayload["pm10"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_standard); + msgPayload["pm25"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_standard); + msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard); + msgPayload["pm10_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental); + msgPayload["pm25_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_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) { + msgPayload["voltage_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_voltage); + msgPayload["current_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_current); + msgPayload["voltage_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_voltage); + msgPayload["current_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_current); + msgPayload["voltage_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_voltage); + 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; + } + 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); + } + 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; + } + 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 + // 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) + + msgPayload["route"] = new JSONValue(route); + 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; + } +#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"); + } + + 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(owner.id); + 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); + if (mp->hop_start != 0 && mp->hop_limit <= mp->hop_start) { + jsonObj["hops_away"] = new JSONValue((unsigned int)(mp->hop_start - mp->hop_limit)); + 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(); + + 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; + + 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); + if (mp->hop_start != 0 && mp->hop_limit <= mp->hop_start) { + jsonObj["hops_away"] = new JSONValue((unsigned int)(mp->hop_start - mp->hop_limit)); + 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(); + + delete value; + return jsonStr; +} +#endif \ No newline at end of file diff --git a/src/serialization/MeshPacketSerializer.h b/src/serialization/MeshPacketSerializer.h new file mode 100644 index 0000000..f248b2b --- /dev/null +++ b/src/serialization/MeshPacketSerializer.h @@ -0,0 +1,24 @@ +#include +#include + +static const char hexChars[16] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; +static const char *errStr = "Error decoding protobuf for %s message!"; + +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; + } +}; \ No newline at end of file diff --git a/src/serialization/MeshPacketSerializer_nRF52.cpp b/src/serialization/MeshPacketSerializer_nRF52.cpp new file mode 100644 index 0000000..6e497f9 --- /dev/null +++ b/src/serialization/MeshPacketSerializer_nRF52.cpp @@ -0,0 +1,353 @@ +#ifdef NRF52_USE_JSON +#warning 'Using nRF52 Serializer' + +#include "ArduinoJson.h" +#include "MeshPacketSerializer.h" +#include "NodeDB.h" +#include "mesh/generated/meshtastic/mqtt.pb.h" +#include "mesh/generated/meshtastic/remote_hardware.pb.h" +#include "mesh/generated/meshtastic/telemetry.pb.h" +#include "modules/RoutingModule.h" +#include +#include + +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(); + + 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) { + jsonObj["payload"]["battery_level"] = (unsigned 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) { + jsonObj["payload"]["temperature"] = decoded->variant.environment_metrics.temperature; + jsonObj["payload"]["relative_humidity"] = decoded->variant.environment_metrics.relative_humidity; + jsonObj["payload"]["barometric_pressure"] = decoded->variant.environment_metrics.barometric_pressure; + jsonObj["payload"]["gas_resistance"] = decoded->variant.environment_metrics.gas_resistance; + jsonObj["payload"]["voltage"] = decoded->variant.environment_metrics.voltage; + jsonObj["payload"]["current"] = decoded->variant.environment_metrics.current; + jsonObj["payload"]["lux"] = decoded->variant.environment_metrics.lux; + jsonObj["payload"]["white_lux"] = decoded->variant.environment_metrics.white_lux; + jsonObj["payload"]["iaq"] = (uint)decoded->variant.environment_metrics.iaq; + jsonObj["payload"]["wind_speed"] = decoded->variant.environment_metrics.wind_speed; + jsonObj["payload"]["wind_direction"] = (uint)decoded->variant.environment_metrics.wind_direction; + jsonObj["payload"]["wind_gust"] = decoded->variant.environment_metrics.wind_gust; + jsonObj["payload"]["wind_lull"] = decoded->variant.environment_metrics.wind_lull; + } else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { + jsonObj["payload"]["pm10"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_standard; + jsonObj["payload"]["pm25"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_standard; + jsonObj["payload"]["pm100"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_standard; + jsonObj["payload"]["pm10_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_environmental; + jsonObj["payload"]["pm25_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_environmental; + jsonObj["payload"]["pm100_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_environmental; + } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { + jsonObj["payload"]["voltage_ch1"] = decoded->variant.power_metrics.ch1_voltage; + jsonObj["payload"]["current_ch1"] = decoded->variant.power_metrics.ch1_current; + jsonObj["payload"]["voltage_ch2"] = decoded->variant.power_metrics.ch2_voltage; + jsonObj["payload"]["current_ch2"] = decoded->variant.power_metrics.ch2_current; + jsonObj["payload"]["voltage_ch3"] = decoded->variant.power_metrics.ch3_voltage; + jsonObj["payload"]["current_ch3"] = decoded->variant.power_metrics.ch3_current; + } + } else if (shouldLog) { + LOG_ERROR("Error decoding protobuf 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 protobuf 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 protobuf 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 protobuf 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 protobuf 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 protobuf 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 protobuf 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"] = owner.id; + if (mp->rx_rssi != 0) + jsonObj["rssi"] = (int)mp->rx_rssi; + if (mp->rx_snr != 0) + jsonObj["snr"] = (float)mp->rx_snr; + if (mp->hop_start != 0 && mp->hop_limit <= mp->hop_start) { + jsonObj["hops_away"] = (unsigned int)(mp->hop_start - mp->hop_limit); + 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; + + if (mp->rx_rssi != 0) + jsonObj["rssi"] = (int)mp->rx_rssi; + if (mp->rx_snr != 0) + jsonObj["snr"] = (float)mp->rx_snr; + if (mp->hop_start != 0 && mp->hop_limit <= mp->hop_start) { + jsonObj["hops_away"] = (unsigned int)(mp->hop_start - mp->hop_limit); + 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); + + return jsonStr; +} +#endif \ No newline at end of file diff --git a/src/shutdown.h b/src/shutdown.h new file mode 100644 index 0000000..4bf3036 --- /dev/null +++ b/src/shutdown.h @@ -0,0 +1,56 @@ +#include "buzz.h" +#include "configuration.h" +#include "graphics/Screen.h" +#include "main.h" +#include "power.h" +#if defined(ARCH_PORTDUINO) +#include "api/WiFiServerAPI.h" +#include "input/LinuxInputImpl.h" + +#endif + +void powerCommandsCheck() +{ + if (rebootAtMsec && millis() > rebootAtMsec) { + LOG_INFO("Rebooting"); +#if defined(ARCH_ESP32) + ESP.restart(); +#elif defined(ARCH_NRF52) + NVIC_SystemReset(); +#elif defined(ARCH_RP2040) + rp2040.reboot(); +#elif defined(ARCH_PORTDUINO) + deInitApiServer(); + if (aLinuxInputImpl) + aLinuxInputImpl->deInit(); + SPI.end(); + Wire.end(); + Serial1.end(); + if (screen) + delete screen; + LOG_DEBUG("final reboot!"); + reboot(); +#else + rebootAtMsec = -1; + LOG_WARN("FIXME implement reboot for this platform. Note that some settings require a restart to be applied."); +#endif + } + +#if defined(ARCH_ESP32) || defined(ARCH_NRF52) + if (shutdownAtMsec) { + screen->startAlert("Shutting down..."); + } +#endif + + if (shutdownAtMsec && millis() > shutdownAtMsec) { + LOG_INFO("Shutting down from admin command"); +#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040) + playShutdownMelody(); + power->shutdown(); +#elif defined(ARCH_PORTDUINO) + exit(EXIT_SUCCESS); +#else + LOG_WARN("FIXME implement shutdown for this platform"); +#endif + } +} \ No newline at end of file diff --git a/src/sleep.cpp b/src/sleep.cpp new file mode 100644 index 0000000..3bc1042 --- /dev/null +++ b/src/sleep.cpp @@ -0,0 +1,515 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_GPS +#include "GPS.h" +#endif + +#include "ButtonThread.h" +#include "Default.h" +#include "Led.h" +#include "MeshRadio.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "PowerMon.h" +#include "detect/LoRaRadioType.h" +#include "error.h" +#include "main.h" +#include "sleep.h" +#include "target_specific.h" + +#ifdef ARCH_ESP32 +// "esp_pm_config_esp32_t is deprecated, please include esp_pm.h and use esp_pm_config_t instead" +#include "esp32/pm.h" +#include "esp_pm.h" +#if HAS_WIFI +#include "mesh/wifi/WiFiAPClient.h" +#endif +#include "rom/rtc.h" +#include +#include + +esp_sleep_source_t wakeCause; // the reason we booted this time +#endif +#include "Throttle.h" + +#ifndef INCLUDE_vTaskSuspend +#define INCLUDE_vTaskSuspend 0 +#endif + +/// Called to ask any observers if they want to veto sleep. Return 1 to veto or 0 to allow sleep to happen +Observable preflightSleep; + +/// Called to tell observers we are now entering sleep and you should prepare. Must return 0 +/// notifySleep will be called for light or deep sleep, notifyDeepSleep is only called for deep sleep +Observable notifySleep, notifyDeepSleep; + +// deep sleep support +RTC_DATA_ATTR int bootCount = 0; + +// ----------------------------------------------------------------------------- +// Application +// ----------------------------------------------------------------------------- + +/** + * Control CPU core speed (80MHz vs 240MHz) + * + * 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) +{ +#if defined(ARCH_ESP32) && HAS_WIFI + + 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("Setting CPU to 240MHz because WiFi is in use."); + setCpuFrequencyMhz(240); +#endif + 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); +#endif + +#endif +} + +// Perform power on init that we do on each wake from deep sleep +void initDeepSleep() +{ +#ifdef ARCH_ESP32 + 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 + + 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]; + */ + +#ifdef DEBUG_PORT + // 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 == TG0WDT_SYS_RESET) + reason = "taskWatchdog"; + + if (hwReason == TG1WDT_SYS_RESET) + reason = "intWatchdog"; + + 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("Disabling 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); + } + } +#endif + +#endif +} + +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) + + 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 + + notifySleep.notifyObservers(NULL); +} + +void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false) +{ + if (INCLUDE_vTaskSuspend && (msecToWake == portMAX_DELAY)) { + LOG_INFO("Entering deep sleep forever"); + } else { + LOG_INFO("Entering 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); + +#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_BLUETOOTH + // Full shutdown of bluetooth hardware + if (nimbleBluetooth) + nimbleBluetooth->deinit(); +#endif + +#ifdef ARCH_ESP32 + if (shouldLoraWake(msecToWake)) { + notifySleep.notifyObservers(NULL); + } else { + notifyDeepSleep.notifyObservers(NULL); + } +#else + notifyDeepSleep.notifyObservers(NULL); +#endif + + powerMon->setState(meshtastic_PowerMon_State_CPU_DeepSleep); + + screen->doDeepSleep(); // datasheet says this will draw only 10ua + + nodeDB->saveToDisk(); + +#ifdef PIN_POWER_EN + pinMode(PIN_POWER_EN, INPUT); // power off peripherals + // pinMode(PIN_POWER_EN1, INPUT_PULLDOWN); +#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); +#endif + +#ifdef BUZZER_EN_PIN + digitalWrite(BUZZER_EN_PIN, LOW); +#endif + +#ifdef PIN_3V3_EN + digitalWrite(PIN_3V3_EN, LOW); +#endif +#endif + ledBlink.set(false); + +#ifdef RESET_OLED + 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 +#endif + +#ifdef ARCH_ESP32 + if (shouldLoraWake(msecToWake)) { + enableLoraInterrupt(); + } +#ifdef BUTTON_PIN + // Avoid leakage through button pin + if (GPIO_IS_VALID_OUTPUT_GPIO(BUTTON_PIN)) { +#ifdef BUTTON_NEED_PULLUP + pinMode(BUTTON_PIN, INPUT_PULLUP); +#else + pinMode(BUTTON_PIN, INPUT); +#endif + gpio_hold_en((gpio_num_t)BUTTON_PIN); + } +#endif + 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 + +#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 listinging 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(); + } + } +#endif + +#if 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); +#endif + + console->flush(); + cpuDeepSleep(msecToWake); +} + +#ifdef ARCH_ESP32 +/** + * enter light sleep (preserves ram but stops everything about CPU). + * + * Returns (after restoring hw state) when the user presses a button or we get a LoRa interrupt + */ +esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more reasonable default +{ + // LOG_DEBUG("Enter light sleep"); + + waitEnterSleep(false); + + uint64_t sleepUsec = sleepMsec * 1000LL; + + // 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); + +#if defined(BUTTON_PIN) && defined(BUTTON_NEED_PULLUP) + 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 + + // 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); + + // doesn't help - I think the USB-UART chip losing power is pulling the signal llow + // 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); +#endif +#ifdef BUTTON_PIN + // The enableLoraInterrupt() method is using ext0_wakeup, so we are forced to use GPIO wakeup + gpio_num_t pin = (gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN); + + // Have to *fully* detach the normal button-interrupts first + buttonThread->detachButtonInterrupts(); + + gpio_wakeup_enable(pin, GPIO_INTR_LOW_LEVEL); + esp_sleep_enable_gpio_wakeup(); +#endif +#ifdef T_WATCH_S3 + gpio_wakeup_enable((gpio_num_t)SCREEN_TOUCH_INT, GPIO_INTR_LOW_LEVEL); +#endif + 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 +#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); + + 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 BUTTON_PIN + // Disable wake-on-button interrupt. Re-attach normal button-interrupts + gpio_wakeup_disable(pin); + buttonThread->attachButtonInterrupts(); +#endif + +#ifdef T_WATCH_S3 + 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); + } +#endif +#if defined(RF95_IRQ) && (RF95_IRQ != RADIOLIB_NC) + if (radioType == RF95_RADIO) { + gpio_wakeup_disable((gpio_num_t)RF95_IRQ); + } +#endif + + esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause(); +#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 +#endif + { + LOG_INFO("Exit light sleep cause: %d", cause); + } + + return cause; +} + +// not legal on the stock android ESP build + +/** + * enable modem sleep mode as needed and available. Should lower our CPU current draw to an average of about 20mA. + * + * per https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/system/power_management.html + * + * supposedly according to https://github.com/espressif/arduino-esp32/issues/475 this is already done in arduino + */ +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 +#else + 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; +#elif CONFIG_IDF_TARGET_ESP32S2 + 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; +#elif CONFIG_IDF_TARGET_ESP32C3 + esp32_config.max_freq_mhz = CONFIG_ESP32C3_DEFAULT_CPU_FREQ_MHZ; +#else + 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); +} + +bool shouldLoraWake(uint32_t msecToWake) +{ + return msecToWake < portMAX_DELAY && (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER); +} + +void enableLoraInterrupt() +{ +#if SOC_PM_SUPPORT_EXT_WAKEUP && defined(LORA_DIO1) && (LORA_DIO1 != RADIOLIB_NC) + gpio_pulldown_en((gpio_num_t)LORA_DIO1); +#if defined(LORA_RESET) && (LORA_RESET != RADIOLIB_NC) + gpio_pullup_en((gpio_num_t)LORA_RESET); +#endif +#if defined(LORA_CS) && (LORA_CS != RADIOLIB_NC) + gpio_pullup_en((gpio_num_t)LORA_CS); +#endif + + if (rtc_gpio_is_valid_gpio((gpio_num_t)LORA_DIO1)) { + // Setup light/deep sleep with wakeup by external source + LOG_INFO("setup LORA_DIO1 (GPIO%02d) with wakeup by external source", LORA_DIO1); + esp_sleep_enable_ext0_wakeup((gpio_num_t)LORA_DIO1, HIGH); + } else { + 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 + } +#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 + } +#endif +} +#endif \ No newline at end of file diff --git a/src/sleep.h b/src/sleep.h new file mode 100644 index 0000000..6ac4207 --- /dev/null +++ b/src/sleep.h @@ -0,0 +1,47 @@ +#pragma once + +#include "Arduino.h" +#include "Observer.h" +#include "configuration.h" + +void doDeepSleep(uint32_t msecToWake, bool skipPreflight), cpuDeepSleep(uint32_t msecToWake); + +#ifdef ARCH_ESP32 +#include "esp_sleep.h" +esp_sleep_wakeup_cause_t doLightSleep(uint64_t msecToWake); + +extern esp_sleep_source_t wakeCause; +#endif + +#ifdef HAS_PMU +#include "XPowersLibInterface.hpp" +extern XPowersLibInterface *PMU; +#endif + +// Perform power on init that we do on each wake from deep sleep +void initDeepSleep(); + +void setCPUFast(bool on); + +/** return true if sleep is allowed right now */ +bool doPreflightSleep(); + +extern int bootCount; + +// is bluetooth sw currently running? +extern bool bluetoothOn; + +/// Called to ask any observers if they want to veto sleep. Return 1 to veto or 0 to allow sleep to happen +extern Observable preflightSleep; + +/// Called to tell observers we are now entering (light or deep) sleep and you should prepare. Must return 0 +extern Observable notifySleep; + +/// Called to tell observers we are now entering (deep) sleep and you should prepare. Must return 0 +extern Observable notifyDeepSleep; + +void enableModemSleep(); +#ifdef ARCH_ESP32 +void enableLoraInterrupt(); +bool shouldLoraWake(uint32_t msecToWake); +#endif \ No newline at end of file diff --git a/src/target_specific.h b/src/target_specific.h new file mode 100644 index 0000000..7404a37 --- /dev/null +++ b/src/target_specific.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +// Functions that are unique to particular target types (esp32, bare, nrf52 etc...) + +// Enable/disable bluetooth. +void setBluetoothEnable(bool enable); + +void getMacAddr(uint8_t *dmac); \ No newline at end of file diff --git a/src/xmodem.cpp b/src/xmodem.cpp new file mode 100644 index 0000000..9eef969 --- /dev/null +++ b/src/xmodem.cpp @@ -0,0 +1,254 @@ +/** + * @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. + * + * 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. + * + * @copyright Copyright (c) 2001-2019 Georges Menie + * @author + * @author + * @date + */ +/*********************************************************************************************************************** + * based on XMODEM implementation by Georges Menie (www.menie.org) + *********************************************************************************************************************** + * Copyright 2001-2019 Georges Menie (www.menie.org) + * All rights reserved. + * + * Adapted for protobuf encapsulation. this is not really Xmodem any more. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the University of California, Berkeley nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **********************************************************************************************************************/ + +#include "xmodem.h" + +#ifdef FSCom + +XModemAdapter xModem; + +XModemAdapter::XModemAdapter() {} + +/** + * Calculates the CRC-16 CCITT checksum of the given buffer. + * + * @param buffer The buffer to calculate the checksum for. + * @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--; + } + + return crc16; +} + +/** + * Calculates the checksum of the given buffer and compares it to the given + * expected checksum. Returns 1 if the checksums match, 0 otherwise. + * + * @param buf The buffer to calculate the checksum of. + * @param sz The size of the buffer. + * @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 Sending control %d.", c); + packetReady.notifyObservers(packetno); +} + +meshtastic_XModem XModemAdapter::getForPhone() +{ + return xmodemStore; +} + +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); + if (xmodemPacket.control == meshtastic_XModem_Control_SOH) { // Receive this file and put to Flash + file = FSCom.open(filename, FILE_O_WRITE); + 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: Transmitting file %s", filename); + file = FSCom.open(filename, FILE_O_READ); + if (file) { + packetno = 1; + isTransmitting = true; + xmodemStore = meshtastic_XModem_init_zero; + xmodemStore.control = meshtastic_XModem_Control_SOH; + xmodemStore.seq = packetno; + xmodemStore.buffer.size = file.read(xmodemStore.buffer.bytes, sizeof(meshtastic_XModem_buffer_t::bytes)); + xmodemStore.crc16 = crc16_ccitt(xmodemStore.buffer.bytes, xmodemStore.buffer.size); + LOG_DEBUG("XModem: STX Notify Sending 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 + file.write(xmodemPacket.buffer.bytes, xmodemPacket.buffer.size); + 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); + file.flush(); + file.close(); + isReceiving = false; + break; + case meshtastic_XModem_Control_CAN: + // Cancel transmission and remove file + sendControl(meshtastic_XModem_Control_ACK); + file.flush(); + file.close(); + FSCom.remove(filename); + isReceiving = false; + break; + case meshtastic_XModem_Control_ACK: + // Acknowledge Send the next packet + if (isTransmitting) { + if (isEOT) { + sendControl(meshtastic_XModem_Control_EOT); + file.close(); + LOG_INFO("XModem: Finished sending 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; + xmodemStore.buffer.size = file.read(xmodemStore.buffer.bytes, sizeof(meshtastic_XModem_buffer_t::bytes)); + xmodemStore.crc16 = crc16_ccitt(xmodemStore.buffer.bytes, xmodemStore.buffer.size); + LOG_DEBUG("XModem: ACK Notify Sending 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); + file.close(); + LOG_INFO("XModem: Retransmit timeout, cancelling file %s", filename); + isTransmitting = false; + break; + } + xmodemStore = meshtastic_XModem_init_zero; + xmodemStore.control = meshtastic_XModem_Control_SOH; + xmodemStore.seq = packetno; + file.seek((packetno - 1) * sizeof(meshtastic_XModem_buffer_t::bytes)); + xmodemStore.buffer.size = file.read(xmodemStore.buffer.bytes, sizeof(meshtastic_XModem_buffer_t::bytes)); + xmodemStore.crc16 = crc16_ccitt(xmodemStore.buffer.bytes, xmodemStore.buffer.size); + LOG_DEBUG("XModem: NAK Notify Sending 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 \ No newline at end of file diff --git a/src/xmodem.h b/src/xmodem.h new file mode 100644 index 0000000..4cfcb43 --- /dev/null +++ b/src/xmodem.h @@ -0,0 +1,80 @@ +/*********************************************************************************************************************** + * based on XMODEM implementation by Georges Menie (www.menie.org) + *********************************************************************************************************************** + * Copyright 2001-2019 Georges Menie (www.menie.org) + * All rights reserved. + * + * Adapted for protobuf encapsulation. this is not really Xmodem any more. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the University of California, Berkeley nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **********************************************************************************************************************/ + +#pragma once + +#include "FSCommon.h" +#include "configuration.h" +#include "mesh/generated/meshtastic/xmodem.pb.h" + +#define MAXRETRANS 25 + +#ifdef FSCom + +class XModemAdapter +{ + public: + // Called when we put a fragment in the outgoing memory + Observable packetReady; + + XModemAdapter(); + + void handlePacket(meshtastic_XModem xmodemPacket); + meshtastic_XModem getForPhone(); + void resetForPhone(); + + private: + bool isReceiving = false; + bool isTransmitting = false; + bool isEOT = false; + + int retrans = MAXRETRANS; + + uint16_t packetno = 0; + +#if defined(ARCH_NRF52) || defined(ARCH_STM32WL) + File file = File(FSCom); +#else + File file; +#endif + + 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); +}; + +extern XModemAdapter xModem; +#endif // FSCom \ No newline at end of file diff --git a/suppressions.txt b/suppressions.txt new file mode 100644 index 0000000..0493752 --- /dev/null +++ b/suppressions.txt @@ -0,0 +1,56 @@ +// cppcheck suppressions +assertWithSideEffect + +// TODO: need to come back to these +duplInheritedMember + +// TODO: +// "Using memset() on struct which contains a floating point number." +// tried: +// if (std::is_floating_point::value) { +// p = 0; +// in src/mesh/MemoryPool.h +memsetClassFloat + +knownConditionTrueFalse + +// no real downside/harm in these +unusedFunction +unusedPrivateFunction + +// most likely due to a cppcheck configuration issue (like missing an include) +syntaxError + +// try to quiet a few +//useInitializationList:src/main.cpp +useInitializationList + +//unreadVariable:src/graphics/Screen.cpp +unreadVariable + +// I don't want to go back and cast function pointers just to appease a tools insatiable thirst for immutability +constParameterCallback + +redundantInitialization + +//cstyleCast:src/mesh/MemoryPool.h:71 +cstyleCast + +// ignore stuff that is not ours +*:.pio/* +*:*/libdeps/* +*:*/generated/* +noExplicitConstructor:*/mqtt/* +postfixOperator:*/mqtt/* + +// these two caused issues +missingOverride +virtualCallInConstructor + +passedByValue:*/RedirectablePrint.h + +internalAstError:*/CrossPlatformCryptoEngine.cpp +uninitMemberVar:*/AudioThread.h +// False positive +constVariableReference:*/Channels.cpp +constParameterPointer:*/unishox2.c \ No newline at end of file diff --git a/test/test_crypto/test_main.cpp b/test/test_crypto/test_main.cpp new file mode 100644 index 0000000..652d5db --- /dev/null +++ b/test/test_crypto/test_main.cpp @@ -0,0 +1,184 @@ +#include "CryptoEngine.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 setUp(void) +{ + // set stuff up here +} + +void tearDown(void) +{ + // clean stuff up here +} + +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(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); +} +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}; + + 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, "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); +} +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, "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, "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_Decrypt(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; + 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"); + fromNode = 0x0929; + crypto->setDHPrivateKey(private_key); + // TEST_ASSERT(crypto->setDHPublicKey(public_key)); + // crypto->hash(crypto->shared_key, 32); + crypto->decryptCurve25519(fromNode, public_key, 0x13b2d662, 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); +} + +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); + + 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); + + 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_Decrypt); +} + +void loop() +{ + UNITY_END(); // stop unit testing +} \ No newline at end of file diff --git a/userPrefs.h b/userPrefs.h new file mode 100644 index 0000000..58a44fe --- /dev/null +++ b/userPrefs.h @@ -0,0 +1,77 @@ +#ifndef _USERPREFS_ +#define _USERPREFS_ + +// Slipstream values: + +#define USERPREFS_TZ_STRING "tzplaceholder " + +// Uncomment and modify to set device defaults + +// #define USERPREFS_EVENT_MODE 1 + +// #define USERPREFS_CONFIG_LORA_REGION meshtastic_Config_LoRaConfig_RegionCode_US +// #define USERPREFS_LORACONFIG_MODEM_PRESET meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST +// #define USERPREFS_LORACONFIG_CHANNEL_NUM 31 +// #define USERPREFS_CONFIG_LORA_IGNORE_MQTT true + +// #define USERPREFS_CONFIG_GPS_MODE meshtastic_Config_PositionConfig_GpsMode_ENABLED + +// #define USERPREFS_CHANNELS_TO_WRITE 3 +/* +#define USERPREFS_CHANNEL_0_PSK \ + { \ + 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 \ + } +*/ +// #define USERPREFS_CHANNEL_0_NAME "DEFCONnect" +// #define USERPREFS_CHANNEL_0_PRECISION 14 +// #define USERPREFS_CHANNEL_0_UPLINK_ENABLED true +// #define USERPREFS_CHANNEL_0_DOWNLINK_ENABLED true +/* +#define USERPREFS_CHANNEL_1_PSK \ + { \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 \ + } +*/ +// #define USERPREFS_CHANNEL_1_NAME "REPLACEME" +// #define USERPREFS_CHANNEL_1_PRECISION 14 +// #define USERPREFS_CHANNEL_1_UPLINK_ENABLED true +// #define USERPREFS_CHANNEL_1_DOWNLINK_ENABLED true +/* +#define USERPREFS_CHANNEL_2_PSK \ + { \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 \ + } +*/ +// #define USERPREFS_CHANNEL_2_NAME "REPLACEME" +// #define USERPREFS_CHANNEL_2_PRECISION 14 +// #define USERPREFS_CHANNEL_2_UPLINK_ENABLED true +// #define USERPREFS_CHANNEL_2_DOWNLINK_ENABLED true + +// #define USERPREFS_CONFIG_OWNER_LONG_NAME "My Long Name" +// #define USERPREFS_CONFIG_OWNER_SHORT_NAME "MLN" + +// #define USERPREFS_SPLASH_TITLE "DEFCONtastic" +// #define icon_width 34 +// #define icon_height 29 +// #define USERPREFS_HAS_SPLASH +/* +static unsigned char icon_bits[] = { + 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, + 0x9E, 0xE7, 0x00, 0x00, 0x00, 0x0E, 0xC7, 0x01, 0x00, 0x1C, 0x0F, 0xC7, 0x01, 0x00, 0x1C, 0xDF, 0xE7, 0x63, 0x00, 0x1C, 0xFF, + 0xBF, 0xE1, 0x00, 0x3C, 0xF3, 0xBF, 0xE3, 0x00, 0x7F, 0xF7, 0xBF, 0xF1, 0x00, 0xFF, 0xF7, 0xBF, 0xF9, 0x03, 0xFF, 0xE7, 0x9F, + 0xFF, 0x03, 0xC0, 0xCF, 0xEF, 0xDF, 0x03, 0x00, 0xDF, 0xE3, 0x8F, 0x00, 0x00, 0x7C, 0xFB, 0x03, 0x00, 0x00, 0xF8, 0xFF, 0x00, + 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x78, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0xFC, 0x00, 0x00, + 0x98, 0x3F, 0xF0, 0x23, 0x00, 0xFC, 0x0F, 0xE0, 0x7F, 0x00, 0xFC, 0x03, 0x80, 0xFF, 0x01, 0xFC, 0x00, 0x00, 0x3E, 0x00, 0x70, + 0x00, 0x00, 0x1C, 0x00, 0x70, 0x00, 0x00, 0x1C, 0x00, 0x70, 0x00, 0x00, 0x1C, 0x00, 0x70, 0x00, 0x00, 0x1C, 0x00}; +*/ +/* +#define USERPREFS_USE_ADMIN_KEY 1 +static unsigned char USERPREFS_ADMIN_KEY[] = {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}; +*/ +#endif \ No newline at end of file diff --git a/userPrefs.json b/userPrefs.json new file mode 100644 index 0000000..bc62602 --- /dev/null +++ b/userPrefs.json @@ -0,0 +1,16 @@ +{ + "USERPREFS_CHANNEL_0_NAME": "\"DEFCONnect\"", + "USERPREFS_CHANNEL_0_PRECISION": "14", + "USERPREFS_CHANNEL_0_PSK": "{ 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 }", + "USERPREFS_CONFIG_LORA_IGNORE_MQTT": "true", + "USERPREFS_CONFIG_LORA_REGION": "meshtastic_Config_LoRaConfig_RegionCode_US", + "USERPREFS_CONFIG_OWNER_LONG_NAME": "\"My Long Name\"", + "USERPREFS_CONFIG_OWNER_SHORT_NAME": "\"MLN\"", + "USERPREFS_EVENT_MODE": "1", + "USERPREFS_HAS_SPLASH": "", + "USERPREFS_LORACONFIG_CHANNEL_NUM": "31", + "USERPREFS_LORACONFIG_MODEM_PRESET": "meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST", + "USERPREFS_SPLASH_TITLE": "\"DEFCONtastic\"", + "USERPREFS_TZ_STRING": "\"tzplaceholder \"", + "USERPREFS_USE_ADMIN_KEY": "1" +} diff --git a/variants/CDEBYTE_EoRa-S3/pins_arduino.h b/variants/CDEBYTE_EoRa-S3/pins_arduino.h new file mode 100644 index 0000000..46415d3 --- /dev/null +++ b/variants/CDEBYTE_EoRa-S3/pins_arduino.h @@ -0,0 +1,28 @@ +// Need this file for ESP32-S3 +// No need to modify this file, changes to pins imported from variant.h +// Most is similar to https://github.com/espressif/arduino-esp32/blob/master/variants/esp32s3/pins_arduino.h + +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// Serial +static const uint8_t TX = UART_TX; +static const uint8_t RX = UART_RX; + +// Default SPI will be mapped to Radio +static const uint8_t SS = LORA_CS; +static const uint8_t SCK = LORA_SCK; +static const uint8_t MOSI = LORA_MOSI; +static const uint8_t MISO = LORA_MISO; + +// The default Wire will be mapped to PMU and RTC +static const uint8_t SCL = I2C_SCL; +static const uint8_t SDA = I2C_SDA; + +#endif /* Pins_Arduino_h */ diff --git a/variants/CDEBYTE_EoRa-S3/platformio.ini b/variants/CDEBYTE_EoRa-S3/platformio.ini new file mode 100644 index 0000000..a1642ff --- /dev/null +++ b/variants/CDEBYTE_EoRa-S3/platformio.ini @@ -0,0 +1,8 @@ +[env:CDEBYTE_EoRa-S3] +extends = esp32s3_base +board = CDEBYTE_EoRa-S3 +build_flags = + ${esp32s3_base.build_flags} + -D CDEBYTE_EORA_S3 + -I variants/CDEBYTE_EoRa-S3 + -D GPS_POWER_TOGGLE \ No newline at end of file diff --git a/variants/CDEBYTE_EoRa-S3/variant.h b/variants/CDEBYTE_EoRa-S3/variant.h new file mode 100644 index 0000000..5da9966 --- /dev/null +++ b/variants/CDEBYTE_EoRa-S3/variant.h @@ -0,0 +1,63 @@ +// LED - status indication +#define LED_PIN 37 + +// Button - user interface +#define BUTTON_PIN 0 // This is the BOOT button, and it has its own pull-up resistor + +// SD card - TODO: test, currently untested, copied from T3S3 variant +#define HAS_SDCARD +#define SDCARD_USE_SPI1 +// TODO: rename this to make this SD-card specific +#define SPI_CS 13 +#define SPI_SCK 14 +#define SPI_MOSI 11 +#define SPI_MISO 2 +// FIXME: there are two other SPI pins that are not defined here +// Compatibility +#define SDCARD_CS SPI_CS + +// 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. + +// Display - OLED connected via I2C by the default hardware configuration +#define HAS_SCREEN 1 +#define USE_SSD1306 +#define I2C_SCL 17 +#define I2C_SDA 18 + +// UART - The 1mm JST SH connector closest to the USB-C port +#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 +#define I2C_SCL1 21 +#define I2C_SDA1 10 + +// Radio +#define USE_SX1262 // CDEBYTE EoRa-S3-900TB <- CDEBYTE E22-900MM22S <- Semtech SX1262 +#define USE_SX1268 // CDEBYTE EoRa-S3-400TB <- CDEBYTE E22-400MM22S <- Semtech SX1268 + +#define SX126X_CS 7 +#define LORA_SCK 5 +#define LORA_MOSI 6 +#define LORA_MISO 3 +#define SX126X_RESET 8 +#define SX126X_BUSY 34 +#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. + +// 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/Dongle_nRF52840-pca10059-v1/platformio.ini b/variants/Dongle_nRF52840-pca10059-v1/platformio.ini new file mode 100644 index 0000000..a98656e --- /dev/null +++ b/variants/Dongle_nRF52840-pca10059-v1/platformio.ini @@ -0,0 +1,14 @@ +[env:pca10059_diy_eink] +board_level = extra +extends = nrf52840_base +board = nordic_pca10059 +build_flags = ${nrf52840_base.build_flags} -Ivariants/Dongle_nRF52840-pca10059-v1 -D NORDIC_PCA10059 + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" + -DEINK_DISPLAY_MODEL=GxEPD2_420_M01 + -DEINK_WIDTH=300 + -DEINK_HEIGHT=400 +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/Dongle_nRF52840-pca10059-v1> +lib_deps = + ${nrf52840_base.lib_deps} + zinggjm/GxEPD2@^1.4.9 +debug_tool = jlink \ No newline at end of file diff --git a/variants/Dongle_nRF52840-pca10059-v1/variant.cpp b/variants/Dongle_nRF52840-pca10059-v1/variant.cpp new file mode 100644 index 0000000..2fc87c7 --- /dev/null +++ b/variants/Dongle_nRF52840-pca10059-v1/variant.cpp @@ -0,0 +1,35 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 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, + + // 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); +} \ No newline at end of file diff --git a/variants/Dongle_nRF52840-pca10059-v1/variant.h b/variants/Dongle_nRF52840-pca10059-v1/variant.h new file mode 100644 index 0000000..2318450 --- /dev/null +++ b/variants/Dongle_nRF52840-pca10059-v1/variant.h @@ -0,0 +1,167 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_NORDIC_PCA10059_ +#define _VARIANT_NORDIC_PCA10059_ + +#define PCA10059 + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (0 + 6) // Built in Green P0.06 +#define PIN_LED2 (0 + 6) // Just here for completeness +#define RGBLED_RED (0 + 8) // Red of RGB P0.08 +#define RGBLED_GREEN (32 + 9) // Green of RGB P1.09 +#define RGBLED_BLUE (0 + 12) // Blue of RGB P0.12 +#define RGBLED_CA // comment out this line if you have a common cathode type, as defined use common anode logic + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 + +#define LED_STATE_ON 0 // State when LED is litted + +/* + * Buttons + */ + +#define PIN_BUTTON1 (32 + 6) // BTN_DN P1.06 Built in button + +/* + * Analog pins + */ +#define PIN_A0 (-1) + +static const uint8_t A0 = PIN_A0; +#define ADC_RESOLUTION 14 + +// Other pins +#define PIN_AREF (-1) // AREF Not yet used + +static const uint8_t AREF = PIN_AREF; + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (-1) +#define PIN_SERIAL1_TX (-1) + +// Connected to Jlink CDC +#define PIN_SERIAL2_RX (-1) +#define PIN_SERIAL2_TX (-1) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +#define PIN_SPI_MISO (0 + 17) // MISO P0.17 +#define PIN_SPI_MOSI (0 + 15) // MOSI P0.15 +#define PIN_SPI_SCK (0 + 13) // SCK P0.13 + +#define PIN_SPI1_MISO (-1) // +#define PIN_SPI1_MOSI (10) // EPD_MOSI P0.10 +#define PIN_SPI1_SCK (9) // EPD_SCLK P0.09 + +static const uint8_t SS = (0 + 31); // LORA_CS P0.31 +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +/* + * eink display pins + */ + +// #define PIN_EINK_EN (-1) +#define PIN_EINK_EN (0 + 6) // Turn on the Green built in LED +#define PIN_EINK_CS (32) // EPD_CS +#define PIN_EINK_BUSY (20) // EPD_BUSY +#define PIN_EINK_DC (24) // EPD_D/C +#define PIN_EINK_RES (-1) // Not Connected P0.22 available +#define PIN_EINK_SCLK (9) // EPD_SCLK +#define PIN_EINK_MOSI (10) // EPD_MOSI + +#define USE_EINK + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (32 + 4) // SDA +#define PIN_WIRE_SCL (32 + 7) // SCL + +// NiceRF 868 LoRa module +#define USE_SX1262 +#define SX126X_CS (0 + 31) // LORA_CS P0.31 +#define SX126X_DIO1 (0 + 29) // DIO1 P0.29 +#define SX126X_BUSY (0 + 2) // LORA_BUSY P0.02 +#define SX126X_RESET (32 + 15) // LORA_RESET P1.15 +#define SX126X_TXEN (32 + 13) // TXEN P1.13 NiceRF 868 dont use +#define SX126X_RXEN (32 + 10) // RXEN P1.10 NiceRF 868 dont use + +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#define PIN_GPS_EN (-1) +#define PIN_GPS_PPS (-1) // Pulse per second input from the GPS + +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +// Battery +// The battery sense is hooked to pin A0 (5) +#define BATTERY_PIN PIN_A0 +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER (1.73F) + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif diff --git a/variants/EBYTE_ESP32-S3/pins_arduino.h b/variants/EBYTE_ESP32-S3/pins_arduino.h new file mode 100644 index 0000000..46415d3 --- /dev/null +++ b/variants/EBYTE_ESP32-S3/pins_arduino.h @@ -0,0 +1,28 @@ +// Need this file for ESP32-S3 +// No need to modify this file, changes to pins imported from variant.h +// Most is similar to https://github.com/espressif/arduino-esp32/blob/master/variants/esp32s3/pins_arduino.h + +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// Serial +static const uint8_t TX = UART_TX; +static const uint8_t RX = UART_RX; + +// Default SPI will be mapped to Radio +static const uint8_t SS = LORA_CS; +static const uint8_t SCK = LORA_SCK; +static const uint8_t MOSI = LORA_MOSI; +static const uint8_t MISO = LORA_MISO; + +// The default Wire will be mapped to PMU and RTC +static const uint8_t SCL = I2C_SCL; +static const uint8_t SDA = I2C_SDA; + +#endif /* Pins_Arduino_h */ diff --git a/variants/EBYTE_ESP32-S3/platformio.ini b/variants/EBYTE_ESP32-S3/platformio.ini new file mode 100644 index 0000000..10de913 --- /dev/null +++ b/variants/EBYTE_ESP32-S3/platformio.ini @@ -0,0 +1,9 @@ +[env:EBYTE_ESP32-S3] +extends = esp32s3_base +; board assumes the lowest spec WROOM module: 4 MB (Quad SPI) Flash, No PSRAM +board = ESP32-S3-WROOM-1-N4 +board_level = extra +build_flags = + ${esp32s3_base.build_flags} + -D EBYTE_ESP32_S3 + -I variants/EBYTE_ESP32-S3 diff --git a/variants/EBYTE_ESP32-S3/variant.h b/variants/EBYTE_ESP32-S3/variant.h new file mode 100644 index 0000000..80fb264 --- /dev/null +++ b/variants/EBYTE_ESP32-S3/variant.h @@ -0,0 +1,193 @@ +// 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 + +// 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. + +// 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) + +// 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 + +// 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 + +////////////////////////////////////////////////////////////////////////////////// +// // +// Have custom connections or functionality? Configure them in this section // +// // +////////////////////////////////////////////////////////////////////////////////// + +#define SX126X_CS 14 // EBYTE module's NSS pin // FIXME: rename to SX126X_SS +#define LORA_SCK 21 // EBYTE module's SCK pin +#define LORA_MOSI 38 // EBYTE module's MOSI pin +#define LORA_MISO 39 // EBYTE module's MISO pin +#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. + +// 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). +/* +#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). +// /* +#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). +/* +#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). +/* +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_TXEN 9 +#define SX126X_RXEN RADIOLIB_NC +*/ + +// Status +#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 +#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 +// Buttons +#define BUTTON_PIN 0 // Use the BOOT button as the user button +// I2C +#define I2C_SCL 18 +#define I2C_SDA 8 +// UART +#define UART_TX 43 +#define UART_RX 44 + +// 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 +// 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. +#define SX126X_MAX_POWER 22 // SX126xInterface.cpp defaults to 22 if not defined, but here we define it for good practice + +// Display +// FIXME: change behavior in src to default to not having screen if is undefined +// FIXME: remove 0/1 option for HAS_SCREEN in src, change to being defined or not +// FIXME: check if it actually causes a crash when not specifiying that a display isn't present +#define HAS_SCREEN 0 // Assume no screen present by default to prevent crash... + +// GPS +// FIXME: unsure what to define HAS_GPS as if GPS isn't always present +#define HAS_GPS 1 // Don't need to set this to 0 to prevent a crash as it doesn't crash if GPS not found, will probe by default +#define PIN_GPS_EN 15 +#define GPS_EN_ACTIVE 1 +#define GPS_TX_PIN 16 +#define GPS_RX_PIN 17 + +///////////////////////////////////////////////////////////////////////////////// +// // +// You should have no need to modify the code below, nor in pins_arduino.h // +// // +///////////////////////////////////////////////////////////////////////////////// + +#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 +/* +// 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 +// https://github.com/jgromes/RadioLib/issues/12#issuecomment-520695575), so set it as such +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#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 diff --git a/variants/ME25LS01-4Y10TD/platformio.ini b/variants/ME25LS01-4Y10TD/platformio.ini new file mode 100644 index 0000000..479a4e7 --- /dev/null +++ b/variants/ME25LS01-4Y10TD/platformio.ini @@ -0,0 +1,15 @@ +[env:ME25LS01-4Y10TD] +extends = nrf52840_base +board = me25ls01-4y10td +board_level = extra +; platform = https://github.com/maxgerhardt/platform-nordicnrf52#cac6fcf943a41accd2aeb4f3659ae297a73f422e +build_flags = ${nrf52840_base.build_flags} -Ivariants/ME25LS01-4Y10TD -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 -DME25LS01_4Y10TD + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. +board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/ME25LS01-4Y10TD> +lib_deps = + ${nrf52840_base.lib_deps} +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +upload_protocol = nrfutil +upload_port = /dev/ttyACM1 \ No newline at end of file diff --git a/variants/ME25LS01-4Y10TD/rfswitch.h b/variants/ME25LS01-4Y10TD/rfswitch.h new file mode 100644 index 0000000..cda6364 --- /dev/null +++ b/variants/ME25LS01-4Y10TD/rfswitch.h @@ -0,0 +1,15 @@ +#include "RadioLib.h" +#include "nrf.h" + +// set RF switch configuration for Wio WM1110 +// Wio WM1110 uses DIO5 and DIO6 for RF switching + +static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; + +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, +}; diff --git a/variants/ME25LS01-4Y10TD/variant.cpp b/variants/ME25LS01-4Y10TD/variant.cpp new file mode 100644 index 0000000..35dc1d3 --- /dev/null +++ b/variants/ME25LS01-4Y10TD/variant.cpp @@ -0,0 +1,40 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 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, + + // 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); + + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); +} \ No newline at end of file diff --git a/variants/ME25LS01-4Y10TD/variant.h b/variants/ME25LS01-4Y10TD/variant.h new file mode 100644 index 0000000..e772069 --- /dev/null +++ b/variants/ME25LS01-4Y10TD/variant.h @@ -0,0 +1,138 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_ME25LS01_4Y10TD_ +#define _VARIANT_ME25LS01_4Y10TD_ + +#define ME25LS01 + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// Use the native nrf52 usb power detection +#define NRF_APM + +#define PIN_3V3_EN (32 + 5) //-1 +#define PIN_3V3_ACC_EN -1 + +#define PIN_LED1 (32 + 7) // P1.07 Blue D2 + +#define LED_PIN PIN_LED1 +#define LED_BUILTIN -1 + +#define LED_BLUE -1 +#define LED_STATE_ON 1 // State when LED is lit + +#define BUTTON_PIN (0 + 27) // P0.27 K3 +#define BUTTON_NEED_PULLUP + +#define HAS_WIRE 1 + +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (0 + 15) // P0.15 +#define PIN_WIRE_SCL (0 + 17) // P0.17 + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (0 + 14) // P0.14 +#define PIN_SERIAL1_TX (0 + 13) // P0.13 + +#define PIN_SERIAL2_RX (0 + 17) // P0.17 +#define PIN_SERIAL2_TX (0 + 16) // P0.16 + +#define SPI_INTERFACES_COUNT 1 + +#define PIN_SPI_MISO (0 + 29) // P0.20 // MISO +#define PIN_SPI_MOSI (0 + 2) // P0.02 // MOSI +#define PIN_SPI_SCK (32 + 15) // P1.15 // SCK +#define PIN_SPI_NSS (32 + 13) // P1.13 // NSS + +#define LORA_RESET (32 + 11) // P1.11 // RST +#define LORA_DIO1 (32 + 12) // P1.12 // IRQ +#define LORA_DIO2 (32 + 10) // P1.10 // BUSY +#define LORA_SCK PIN_SPI_SCK +#define LORA_MISO PIN_SPI_MISO +#define LORA_MOSI PIN_SPI_MOSI +#define LORA_CS PIN_SPI_NSS + +// supported modules list +#define USE_LR1110 + +#define LR1110_IRQ_PIN LORA_DIO1 +#define LR1110_NRESET_PIN LORA_RESET +#define LR1110_BUSY_PIN LORA_DIO2 +#define LR1110_SPI_NSS_PIN LORA_CS +#define LR1110_SPI_SCK_PIN LORA_SCK +#define LR1110_SPI_MOSI_PIN LORA_MOSI +#define LR1110_SPI_MISO_PIN LORA_MISO + +#define LR11X0_DIO3_TCXO_VOLTAGE 1.6 +#define LR11X0_DIO_AS_RF_SWITCH + +#define HAS_GPS 0 + +#define PIN_GPS_EN -1 +#define GPS_EN_ACTIVE HIGH +#define PIN_GPS_RESET -1 +#define GPS_VRTC_EN -1 +#define GPS_SLEEP_INT -1 +#define GPS_RTC_INT -1 +#define GPS_RESETB_OUT -1 + +#define BATTERY_PIN -1 +#define ADC_MULTIPLIER (2.0F) + +#define ADC_RESOLUTION 14 +#define BATTERY_SENSE_RESOLUTION_BITS 12 + +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 + +// Buzzer +#define PIN_BUZZER (0 + 25) + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif // _VARIANT_ME25LS01_4Y10TD_ diff --git a/variants/ME25LS01-4Y10TD_e-ink/platformio.ini b/variants/ME25LS01-4Y10TD_e-ink/platformio.ini new file mode 100644 index 0000000..f2e3a49 --- /dev/null +++ b/variants/ME25LS01-4Y10TD_e-ink/platformio.ini @@ -0,0 +1,19 @@ +[env:ME25LS01-4Y10TD_e-ink] +extends = nrf52840_base +board = me25ls01-4y10td +board_level = extra +; platform = https://github.com/maxgerhardt/platform-nordicnrf52#cac6fcf943a41accd2aeb4f3659ae297a73f422e +build_flags = ${nrf52840_base.build_flags} -Ivariants/ME25LS01-4Y10TD_e-ink -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 -DME25LS01_4Y10TD + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + -DEINK_DISPLAY_MODEL=GxEPD2_420_GDEY042T81 + -DEINK_WIDTH=400 + -DEINK_HEIGHT=300 +board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/ME25LS01-4Y10TD_e-ink> +lib_deps = + ${nrf52840_base.lib_deps} + zinggjm/GxEPD2@^1.5.8 +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +upload_protocol = nrfutil +upload_port = /dev/ttyACM1 \ No newline at end of file diff --git a/variants/ME25LS01-4Y10TD_e-ink/rfswitch.h b/variants/ME25LS01-4Y10TD_e-ink/rfswitch.h new file mode 100644 index 0000000..cda6364 --- /dev/null +++ b/variants/ME25LS01-4Y10TD_e-ink/rfswitch.h @@ -0,0 +1,15 @@ +#include "RadioLib.h" +#include "nrf.h" + +// set RF switch configuration for Wio WM1110 +// Wio WM1110 uses DIO5 and DIO6 for RF switching + +static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; + +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, +}; diff --git a/variants/ME25LS01-4Y10TD_e-ink/variant.cpp b/variants/ME25LS01-4Y10TD_e-ink/variant.cpp new file mode 100644 index 0000000..35dc1d3 --- /dev/null +++ b/variants/ME25LS01-4Y10TD_e-ink/variant.cpp @@ -0,0 +1,40 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 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, + + // 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); + + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); +} \ No newline at end of file diff --git a/variants/ME25LS01-4Y10TD_e-ink/variant.h b/variants/ME25LS01-4Y10TD_e-ink/variant.h new file mode 100644 index 0000000..797394c --- /dev/null +++ b/variants/ME25LS01-4Y10TD_e-ink/variant.h @@ -0,0 +1,161 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_ME25LS01_4Y10TD_ +#define _VARIANT_ME25LS01_4Y10TD_ + +#define ME25LS01 + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// Use the native nrf52 usb power detection +#define NRF_APM + +#define PIN_3V3_EN (32 + 5) //-1 +#define PIN_3V3_ACC_EN -1 + +#define PIN_LED1 (32 + 7) // P1.07 Blue D2 + +#define LED_PIN PIN_LED1 +#define LED_BUILTIN -1 + +#define LED_BLUE -1 +#define LED_STATE_ON 1 // State when LED is lit + +#define BUTTON_PIN (0 + 27) // P0.27 K3 +#define BUTTON_NEED_PULLUP + +#define HAS_WIRE 1 + +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (0 + 15) // P0.15 +#define PIN_WIRE_SCL (0 + 17) // P0.17 + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (0 + 14) // P0.14 +#define PIN_SERIAL1_TX (0 + 13) // P0.13 + +#define PIN_SERIAL2_RX (0 + 17) // P0.17 +#define PIN_SERIAL2_TX (0 + 16) // P0.16 + +#define SPI_INTERFACES_COUNT 2 + +// LoRa SPI +#define PIN_SPI_MISO (0 + 29) // P0.20 // MISO +#define PIN_SPI_MOSI (0 + 2) // P0.02 // MOSI +#define PIN_SPI_SCK (32 + 15) // P1.15 // SCK +#define PIN_SPI_NSS (32 + 13) // P1.13 // NSS + +#define LORA_RESET (32 + 11) // P1.11 // RST +#define LORA_DIO1 (32 + 12) // P1.12 // IRQ +#define LORA_DIO2 (32 + 10) // P1.10 // BUSY +#define LORA_SCK PIN_SPI_SCK +#define LORA_MISO PIN_SPI_MISO +#define LORA_MOSI PIN_SPI_MOSI +#define LORA_CS PIN_SPI_NSS + +static const uint8_t SS = (32 + 13); // P1.13 // NSS +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +// EPD SPI +#define PIN_SPI1_MISO (32 + 2) // Not Used for EPD but needs to be defined +#define PIN_SPI1_MOSI (0 + 10) // EPD_MOSI P0.10 +#define PIN_SPI1_SCK (0 + 9) // EPD_SCLK P0.09 + +/* + * eink display pins + */ + +#define USE_EINK +#define PIN_EINK_CS (32 + 0) // EPD_CS +#define PIN_EINK_BUSY (0 + 19) // EPD_BUSY +#define PIN_EINK_DC (0 + 24) // EPD_D/C +#define PIN_EINK_RES (0 + 23) // EPD_RESET +#define PIN_EINK_SCLK PIN_SPI1_SCK +#define PIN_EINK_MOSI PIN_SPI1_MOSI + +// supported modules list +#define USE_LR1110 + +#define LR1110_IRQ_PIN LORA_DIO1 +#define LR1110_NRESET_PIN LORA_RESET +#define LR1110_BUSY_PIN LORA_DIO2 +#define LR1110_SPI_NSS_PIN LORA_CS +#define LR1110_SPI_SCK_PIN LORA_SCK +#define LR1110_SPI_MOSI_PIN LORA_MOSI +#define LR1110_SPI_MISO_PIN LORA_MISO + +#define LR11X0_DIO3_TCXO_VOLTAGE 1.6 +#define LR11X0_DIO_AS_RF_SWITCH + +#define HAS_GPS 0 + +#define PIN_GPS_EN -1 +#define GPS_EN_ACTIVE HIGH +#define PIN_GPS_RESET -1 +#define GPS_VRTC_EN -1 +#define GPS_SLEEP_INT -1 +#define GPS_RTC_INT -1 +#define GPS_RESETB_OUT -1 + +#define BATTERY_PIN -1 +#define ADC_MULTIPLIER (2.0F) + +#define ADC_RESOLUTION 14 +#define BATTERY_SENSE_RESOLUTION_BITS 12 + +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 + +// Buzzer +#define PIN_BUZZER (0 + 25) + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif // _VARIANT_ME25LS01_4Y10TD__ \ No newline at end of file diff --git a/variants/MS24SF1/platformio.ini b/variants/MS24SF1/platformio.ini new file mode 100644 index 0000000..5cbd078 --- /dev/null +++ b/variants/MS24SF1/platformio.ini @@ -0,0 +1,15 @@ +[env:ms24sf1] +extends = nrf52840_base +board = ms24sf1 +board_level = extra +; platform = https://github.com/maxgerhardt/platform-nordicnrf52#cac6fcf943a41accd2aeb4f3659ae297a73f422e +build_flags = ${nrf52840_base.build_flags} -Ivariants/MS24SF1 -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. +board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/MS24SF1> +lib_deps = + ${nrf52840_base.lib_deps} +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +upload_protocol = nrfutil +upload_port = /dev/ttyACM1 diff --git a/variants/MS24SF1/variant.cpp b/variants/MS24SF1/variant.cpp new file mode 100644 index 0000000..d1d79c8 --- /dev/null +++ b/variants/MS24SF1/variant.cpp @@ -0,0 +1,30 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 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, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() {} diff --git a/variants/MS24SF1/variant.h b/variants/MS24SF1/variant.h new file mode 100644 index 0000000..d26dceb --- /dev/null +++ b/variants/MS24SF1/variant.h @@ -0,0 +1,139 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_MINEWSEMI_MS24SF1_ +#define _VARIANT_MINEWSEMI_MS24SF1_ + +#define ME25LS01 + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// Use the native nrf52 usb power detection +#define NRF_APM + +#define PIN_3V3_EN (32 + 5) //-1 +#define PIN_3V3_ACC_EN -1 + +#define PIN_LED1 (-1) + +#define LED_PIN PIN_LED1 +#define LED_BUILTIN -1 + +#define LED_BLUE -1 +#define LED_STATE_ON 1 // State when LED is lit + +#define BUTTON_PIN (-1) +#define BUTTON_NEED_PULLUP + +#define HAS_WIRE 1 + +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (0 + 29) // P0.15 +#define PIN_WIRE_SCL (0 + 30) // P0.17 + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (-1) // P0.14 +#define PIN_SERIAL1_TX (-1) // P0.13 + +#define PIN_SERIAL2_RX (-1) // P0.17 +#define PIN_SERIAL2_TX (-1) // P0.16 + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 1 // 2 + +#define PIN_SPI_MISO (0 + 17) // MISO P0.17 +#define PIN_SPI_MOSI (0 + 20) // MOSI P0.20 +#define PIN_SPI_SCK (0 + 21) // SCK P0.21 + +// #define PIN_SPI1_MISO (-1) // +// #define PIN_SPI1_MOSI (10) // EPD_MOSI P0.10 +// #define PIN_SPI1_SCK (9) // EPD_SCLK P0.09 + +static const uint8_t SS = (0 + 22); // LORA_CS P0.22 +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +// MINEWSEMI nRF52840+SX1262 MS24SF1 (NRF82540 with integrated SX1262) +#define USE_SX1262 +#define SX126X_CS (0 + 22) // LORA_CS P0.22 +#define SX126X_DIO1 (0 + 16) // DIO1 P0.16 +#define SX126X_BUSY (0 + 19) // LORA_BUSY P0.19 +#define SX126X_RESET (0 + 12) // LORA_RESET P0.12 +#define SX126X_TXEN (32 + 4) // TXEN P1.04 +#define SX126X_RXEN (32 + 2) // RXEN P1.02 + +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH + +#define HAS_GPS 0 + +#define PIN_GPS_EN -1 +#define GPS_EN_ACTIVE HIGH +#define PIN_GPS_RESET -1 +#define GPS_VRTC_EN -1 +#define GPS_SLEEP_INT -1 +#define GPS_RTC_INT -1 +#define GPS_RESETB_OUT -1 + +#define BATTERY_PIN -1 +#define ADC_MULTIPLIER (2.0F) + +#define ADC_RESOLUTION 14 +#define BATTERY_SENSE_RESOLUTION_BITS 12 + +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 + +// Buzzer +// #define PIN_BUZZER (0 + 25) + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif // _VARIANT_MINEWSEMI_MS24SF1_ diff --git a/variants/MakePython_nRF52840_eink/platformio.ini b/variants/MakePython_nRF52840_eink/platformio.ini new file mode 100644 index 0000000..b11b54c --- /dev/null +++ b/variants/MakePython_nRF52840_eink/platformio.ini @@ -0,0 +1,17 @@ +[env:makerpython_nrf52840_sx1280_eink] +board_level = extra +extends = nrf52840_base +board = nordic_pca10059 +build_flags = ${nrf52840_base.build_flags} -Ivariants/MakePython_nRF52840_eink -D PRIVATE_HW + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" + -D PIN_EINK_EN +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/MakePython_nRF52840_eink> +lib_deps = + ${nrf52840_base.lib_deps} + https://github.com/meshtastic/ESP32_Codec2.git#633326c78ac251c059ab3a8c430fcdf25b41672f + zinggjm/GxEPD2@^1.4.9 + -DEINK_DISPLAY_MODEL=GxEPD2_290_T5D + -DEINK_WIDTH=296 + -DEINK_HEIGHT=128 +debug_tool = jlink +;upload_port = /dev/ttyACM4 diff --git a/variants/MakePython_nRF52840_eink/variant.cpp b/variants/MakePython_nRF52840_eink/variant.cpp new file mode 100644 index 0000000..8c6bf03 --- /dev/null +++ b/variants/MakePython_nRF52840_eink/variant.cpp @@ -0,0 +1,38 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 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, + + // 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); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); +} diff --git a/variants/MakePython_nRF52840_eink/variant.h b/variants/MakePython_nRF52840_eink/variant.h new file mode 100644 index 0000000..00c8dc1 --- /dev/null +++ b/variants/MakePython_nRF52840_eink/variant.h @@ -0,0 +1,148 @@ +#ifndef _VARIANT_MAKERPYTHON_NRF82540_EINK_ +#define _VARIANT_MAKERPYTHON_NRF82540_EINK_ + +#define MAKERPYTHON + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (32 + 10) // LED P1.15 +#define PIN_LED2 (-1) // + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 + +#define LED_STATE_ON 0 // State when LED is litted + +/* + * Buttons + */ + +#define PIN_BUTTON1 (32 + 15) // P1.15 Built in button + +/* + * Analog pins + */ +#define PIN_A0 (-1) + +static const uint8_t A0 = PIN_A0; +#define ADC_RESOLUTION 14 + +// Other pins +#define PIN_AREF (-1) // AREF Not yet used + +static const uint8_t AREF = PIN_AREF; + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (-1) +#define PIN_SERIAL1_TX (-1) + +// Connected to Jlink CDC +#define PIN_SERIAL2_RX (-1) +#define PIN_SERIAL2_TX (-1) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 +// here +// #define SPI_INTERFACES_COUNT 1 + +#define PIN_SPI_MISO (0 + 31) // MISO P0.31 +#define PIN_SPI_MOSI (0 + 30) // MOSI P0.30 +#define PIN_SPI_SCK (0 + 29) // SCK P0.29 + +// here +#define PIN_SPI1_MISO (-1) // +#define PIN_SPI1_MOSI (0 + 28) // EPD_MOSI P0.10 +#define PIN_SPI1_SCK (0 + 2) // EPD_SCLK P0.09 + +static const uint8_t SS = (32 + 15); // LORA_CS P1.15 +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +// here +/* + * eink display pins + */ + +// #define PIN_EINK_EN (-1) +#define PIN_EINK_CS (0 + 3) // EPD_CS +#define PIN_EINK_BUSY (32 + 11) // EPD_BUSY +#define PIN_EINK_DC (32 + 13) // EPD_D/C +#define PIN_EINK_RES (-1) // Not used +#define PIN_EINK_SCLK (0 + 2) // EPD_SCLK +#define PIN_EINK_MOSI (0 + 28) // EPD_MOSI + +#define USE_EINK + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (0 + 21) // SDA +#define PIN_WIRE_SCL (0 + 22) // SCL + +// E-Byte E28 2.4 Ghz LoRa module +#define USE_SX1280 +#define LORA_RESET (0 + 5) +#define SX128X_CS (0 + 23) +#define SX128X_DIO1 (0 + 4) +#define SX128X_BUSY (0 + 7) +// #define SX128X_TXEN (32 + 9) +// #define SX128X_RXEN (0 + 12) +#define SX128X_RESET LORA_RESET + +#define PIN_GPS_EN (-1) +#define PIN_GPS_PPS (-1) // Pulse per second input from the GPS + +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +// Battery +// The battery sense is hooked to pin A0 (5) +#define BATTERY_PIN PIN_A0 +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER (1.73F) + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif \ No newline at end of file diff --git a/variants/MakePython_nRF52840_oled/platformio.ini b/variants/MakePython_nRF52840_oled/platformio.ini new file mode 100644 index 0000000..0146385 --- /dev/null +++ b/variants/MakePython_nRF52840_oled/platformio.ini @@ -0,0 +1,11 @@ +[env:makerpython_nrf52840_sx1280_oled] +board_level = extra +extends = nrf52840_base +board = nordic_pca10059 +build_flags = ${nrf52840_base.build_flags} -Ivariants/MakePython_nRF52840_oled -D PRIVATE_HW + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/MakePython_nRF52840_oled> +lib_deps = + ${nrf52840_base.lib_deps} + https://github.com/meshtastic/ESP32_Codec2.git#633326c78ac251c059ab3a8c430fcdf25b41672f +debug_tool = jlink diff --git a/variants/MakePython_nRF52840_oled/variant.cpp b/variants/MakePython_nRF52840_oled/variant.cpp new file mode 100644 index 0000000..8c6bf03 --- /dev/null +++ b/variants/MakePython_nRF52840_oled/variant.cpp @@ -0,0 +1,38 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 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, + + // 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); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); +} diff --git a/variants/MakePython_nRF52840_oled/variant.h b/variants/MakePython_nRF52840_oled/variant.h new file mode 100644 index 0000000..28d9411 --- /dev/null +++ b/variants/MakePython_nRF52840_oled/variant.h @@ -0,0 +1,126 @@ +#ifndef _VARIANT_MAKERPYTHON_NRF82540_OLED_ +#define _VARIANT_MAKERPYTHON_NRF82540_OLED_ + +#define MAKERPYTHON + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (32 + 10) // LED P1.15 +#define PIN_LED2 (-1) // + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 + +#define LED_STATE_ON 0 // State when LED is litted + +/* + * Buttons + */ + +#define PIN_BUTTON1 (32 + 15) // P1.15 Built in button + +/* + * Analog pins + */ +#define PIN_A0 (-1) + +static const uint8_t A0 = PIN_A0; +#define ADC_RESOLUTION 14 + +// Other pins +#define PIN_AREF (-1) // AREF Not yet used + +static const uint8_t AREF = PIN_AREF; + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (-1) +#define PIN_SERIAL1_TX (-1) + +// Connected to Jlink CDC +#define PIN_SERIAL2_RX (-1) +#define PIN_SERIAL2_TX (-1) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 1 + +#define PIN_SPI_MISO (0 + 31) // MISO P0.31 +#define PIN_SPI_MOSI (0 + 30) // MOSI P0.30 +#define PIN_SPI_SCK (0 + 29) // SCK P0.29 + +static const uint8_t SS = (32 + 15); // LORA_CS P1.15 +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (0 + 26) // SDA +#define PIN_WIRE_SCL (0 + 27) // SCL + +// E-Byte E28 2.4 Ghz LoRa module +#define USE_SX1280 +#define LORA_RESET (0 + 5) +#define SX128X_CS (0 + 23) +#define SX128X_DIO1 (0 + 4) +#define SX128X_BUSY (0 + 7) +// #define SX128X_TXEN (32 + 9) +// #define SX128X_RXEN (0 + 12) +#define SX128X_RESET LORA_RESET + +#define PIN_GPS_EN (-1) +#define PIN_GPS_PPS (-1) // Pulse per second input from the GPS + +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +// Battery +// The battery sense is hooked to pin A0 (5) +#define BATTERY_PIN PIN_A0 +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER (1.73F) + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif \ No newline at end of file diff --git a/variants/TWC_mesh_v4/platformio.ini b/variants/TWC_mesh_v4/platformio.ini new file mode 100644 index 0000000..4fb3823 --- /dev/null +++ b/variants/TWC_mesh_v4/platformio.ini @@ -0,0 +1,10 @@ +[env:TWC_mesh_v4] +extends = nrf52840_base +board = nordic_pca10059 +board_level = extra +build_flags = ${nrf52840_base.build_flags} -I variants/TWC_mesh_v4 -D TWC_mesh_v4 -L".pio\libdeps\TWC_mesh_v4\bsec2\src\cortex-m4\fpv4-sp-d16-hard" +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/TWC_mesh_v4> +lib_deps = + ${nrf52840_base.lib_deps} + zinggjm/GxEPD2@^1.4.9 +debug_tool = jlink \ No newline at end of file diff --git a/variants/TWC_mesh_v4/variant.cpp b/variants/TWC_mesh_v4/variant.cpp new file mode 100644 index 0000000..b371234 --- /dev/null +++ b/variants/TWC_mesh_v4/variant.cpp @@ -0,0 +1,38 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 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, + + // 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); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); +} \ No newline at end of file diff --git a/variants/TWC_mesh_v4/variant.h b/variants/TWC_mesh_v4/variant.h new file mode 100644 index 0000000..6a6f541 --- /dev/null +++ b/variants/TWC_mesh_v4/variant.h @@ -0,0 +1,133 @@ +#ifndef _VARIANT_TWC_MESH_V4_ +#define _VARIANT_TWC_MESH_V4_ + +#define PCA10059 + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (32 + 10) // Blue LED P1.10 +#define PIN_LED2 (32 + 15) // Built in Green P1.15 + +// RGB NeoPixel LED2 +// #define PIN_LED1 (0 + 8) Red +// #define PIN_LED1 (32 + 9) Green +// #define PIN_LED1 (0 + 12) Blue + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 + +#define LED_STATE_ON 0 // State when LED is litted + +/* + * Buttons + */ +#define PIN_BUTTON1 (32 + 2) // BTN_DN P1.02 Built in button + +/* + * Analog pins + */ +#define PIN_A0 (0 + 29) // using VDIV (A6 / P0.29) + +static const uint8_t A0 = PIN_A0; +#define ADC_RESOLUTION 14 + +// Other pins +#define PIN_AREF (-1) // AREF Not yet used + +static const uint8_t AREF = PIN_AREF; + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (0 + 24) +#define PIN_SERIAL1_TX (0 + 25) + +// Connected to Jlink CDC +#define PIN_SERIAL2_RX (-1) +#define PIN_SERIAL2_TX (-1) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 1 + +#define PIN_SPI_MISO (0 + 15) // MISO P0.15 +#define PIN_SPI_MOSI (0 + 13) // MOSI P0.13 +#define PIN_SPI_SCK (0 + 14) // SCK P0.14 + +static const uint8_t SS = (0 + 6); // LORA_CS P0.6 +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +////#define USE_EINK +#define USE_SSD1306 + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (0 + 12) // SDA P0.12 +#define PIN_WIRE_SCL (0 + 11) // SCL P0.11 + +// NiceRF 868 LoRa module +#define USE_SX1262 +#define USE_LLCC68 + +#define SX126X_CS (0 + 6) // LORA_CS P0.06 +#define SX126X_DIO1 (0 + 7) // DIO1 P0.07 +#define SX126X_BUSY (0 + 26) // LORA_BUSY P0.26 +#define SX126X_RESET (0 + 27) // LORA_RESET P0.27 +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#define PIN_GPS_EN (-1) +#define PIN_GPS_PPS (-1) // Pulse per second input from the GPS + +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +// Battery +// The battery sense is hooked to pin A6 (0.29) +#define BATTERY_PIN PIN_A0 +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER (2.0F) + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif \ No newline at end of file diff --git a/variants/ai-c3/platformio.ini b/variants/ai-c3/platformio.ini new file mode 100644 index 0000000..2869ca5 --- /dev/null +++ b/variants/ai-c3/platformio.ini @@ -0,0 +1,8 @@ +[env:ai-c3] +extends = esp32c3_base +board = esp32-c3-devkitm-1 +board_level = extra +build_flags = ${esp32c3_base.build_flags} + -D PRIVATE_HW + -I variants/ai-c3 + diff --git a/variants/ai-c3/variant.h b/variants/ai-c3/variant.h new file mode 100644 index 0000000..6c4f4d3 --- /dev/null +++ b/variants/ai-c3/variant.h @@ -0,0 +1,32 @@ +#define SDA 0 +#define SCL 1 +#define I2C_SDA SDA +#define I2C_SCL SCL + +#define BUTTON_PIN 9 // BOOT button +#define LED_PIN 30 // RGB LED + +#define USE_RF95 +#define LORA_SCK 4 +#define LORA_MISO 5 +#define LORA_MOSI 6 +#define LORA_CS 7 + +#define LORA_DIO0 10 +#define LORA_DIO1 3 +#define LORA_RESET 2 + +// WaveShare Core1262-868M +// https://www.waveshare.com/wiki/Core1262-868M +#define USE_SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY 10 +#define SX126X_RESET LORA_RESET + +#define SX126X_DIO2_AS_RF_SWITCH // use DIO2 as RF switch +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#define HAS_GPS 0 +#undef GPS_RX_PIN +#undef GPS_TX_PIN diff --git a/variants/betafpv_2400_tx_micro/platformio.ini b/variants/betafpv_2400_tx_micro/platformio.ini new file mode 100644 index 0000000..531e853 --- /dev/null +++ b/variants/betafpv_2400_tx_micro/platformio.ini @@ -0,0 +1,18 @@ +[env:betafpv_2400_tx_micro] +extends = esp32_base +board = esp32doit-devkit-v1 +board_level = extra +build_flags = + ${esp32_base.build_flags} + -D BETAFPV_2400_TX + -D VTABLES_IN_FLASH=1 + -D CONFIG_DISABLE_HAL_LOCKS=1 + -O2 + -I variants/betafpv_2400_tx_micro +board_build.f_cpu = 240000000L +upload_protocol = esptool +;upload_port = /dev/ttyUSB0 +upload_speed = 460800 +lib_deps = + ${esp32_base.lib_deps} + adafruit/Adafruit NeoPixel @ ^1.12.0 \ No newline at end of file diff --git a/variants/betafpv_2400_tx_micro/variant.h b/variants/betafpv_2400_tx_micro/variant.h new file mode 100644 index 0000000..67699e7 --- /dev/null +++ b/variants/betafpv_2400_tx_micro/variant.h @@ -0,0 +1,37 @@ +// https://betafpv.com/products/elrs-micro-tx-module + +// 0.96" OLED +#define I2C_SDA 22 +#define I2C_SCL 32 + +// NO GPS +#undef GPS_RX_PIN +#undef GPS_TX_PIN + +#define LORA_SCK 18 +#define LORA_MISO 19 +#define LORA_MOSI 23 +#define LORA_CS 5 +#define RF95_FAN_EN 17 + +// #define LED_PIN 16 // This is a LED_WS2812 not a standard LED +#define HAS_NEOPIXEL // Enable the use of neopixels +#define NEOPIXEL_COUNT 1 // How many neopixels are connected +#define NEOPIXEL_DATA 16 // gpio pin used to send data to the neopixels +#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use + +#define BUTTON_PIN 25 +#define BUTTON_NEED_PULLUP + +#undef EXT_NOTIFY_OUT + +// SX128X 2.4 Ghz LoRa module +#define USE_SX1280 +#define LORA_RESET 14 +#define SX128X_CS 5 +#define SX128X_DIO1 4 +#define SX128X_BUSY 21 +#define SX128X_TXEN 26 +#define SX128X_RXEN 27 +#define SX128X_RESET LORA_RESET +#define SX128X_MAX_POWER 3 diff --git a/variants/betafpv_900_tx_nano/platformio.ini b/variants/betafpv_900_tx_nano/platformio.ini new file mode 100644 index 0000000..3bea16f --- /dev/null +++ b/variants/betafpv_900_tx_nano/platformio.ini @@ -0,0 +1,17 @@ +[env:betafpv_900_tx_nano] +extends = esp32_base +board = esp32doit-devkit-v1 +board_level = extra +build_flags = + ${esp32_base.build_flags} + -D BETAFPV_900_TX_NANO + -D VTABLES_IN_FLASH=1 + -D CONFIG_DISABLE_HAL_LOCKS=1 + -O2 + -I variants/betafpv_900_tx_nano +board_build.f_cpu = 240000000L +upload_protocol = esptool +;upload_port = /dev/ttyUSB0 +upload_speed = 460800 +lib_deps = + ${esp32_base.lib_deps} \ No newline at end of file diff --git a/variants/betafpv_900_tx_nano/variant.h b/variants/betafpv_900_tx_nano/variant.h new file mode 100644 index 0000000..7a4ae91 --- /dev/null +++ b/variants/betafpv_900_tx_nano/variant.h @@ -0,0 +1,28 @@ +// https://betafpv.com/products/elrs-nano-tx-module + +// no screen +#define HAS_SCREEN 0 + +// NO GPS +#undef GPS_RX_PIN +#undef GPS_TX_PIN + +#define USE_RF95 + +#define LORA_SCK 18 +#define LORA_MISO 19 +#define LORA_MOSI 23 +#define LORA_CS 5 + +#define LORA_DIO0 4 +#define LORA_RESET 14 +#define LORA_DIO1 2 +#define LORA_DIO2 +#define LORA_DIO3 + +#define LED_PIN 16 // green - blue is at 17 + +#define BUTTON_PIN 25 +#define BUTTON_NEED_PULLUP + +#undef EXT_NOTIFY_OUT diff --git a/variants/bpi_picow_esp32_s3/pins_arduino.h b/variants/bpi_picow_esp32_s3/pins_arduino.h new file mode 100644 index 0000000..dd7b3c5 --- /dev/null +++ b/variants/bpi_picow_esp32_s3/pins_arduino.h @@ -0,0 +1,29 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +// The default Wire will be mapped to PMU and RTC +static const uint8_t SDA = 12; +static const uint8_t SCL = 14; + +// Default SPI will be mapped to Radio +static const uint8_t MISO = 39; +static const uint8_t SCK = 21; +static const uint8_t MOSI = 38; +static const uint8_t SS = 17; + +// #define SPI_MOSI (11) +// #define SPI_SCK (14) +// #define SPI_MISO (2) +// #define SPI_CS (13) + +// #define SDCARD_CS SPI_CS + +#endif /* Pins_Arduino_h */ diff --git a/variants/bpi_picow_esp32_s3/platformio.ini b/variants/bpi_picow_esp32_s3/platformio.ini new file mode 100644 index 0000000..7e94cc9 --- /dev/null +++ b/variants/bpi_picow_esp32_s3/platformio.ini @@ -0,0 +1,14 @@ +[env:bpi_picow_esp32_s3] +extends = esp32s3_base +board = bpi_picow_esp32_s3 +board_level = extra +;OpenOCD flash method +;upload_protocol = esp-builtin +;Normal method +upload_protocol = esptool +;upload_port = /dev/ttyACM2 +lib_deps = + ${esp32_base.lib_deps} + caveman99/ESP32 Codec2@^1.0.1 +build_flags = + ${esp32_base.build_flags} -D PRIVATE_HW -I variants/bpi_picow_esp32_s3 \ No newline at end of file diff --git a/variants/bpi_picow_esp32_s3/variant.h b/variants/bpi_picow_esp32_s3/variant.h new file mode 100644 index 0000000..d8d9413 --- /dev/null +++ b/variants/bpi_picow_esp32_s3/variant.h @@ -0,0 +1,74 @@ +#define HAS_GPS 0 +#undef GPS_RX_PIN +#undef GPS_TX_PIN + +// #define HAS_SCREEN 0 + +// #define HAS_SDCARD +// #define SDCARD_USE_SPI1 + +#define USE_SSD1306 +#define I2C_SDA 12 +#define I2C_SCL 14 + +#define LED_PIN 46 +#define LED_STATE_ON 0 // State when LED is litted + +// #define BUTTON_PIN 15 // Pico OLED 1.3 User key 0 - removed User key 1 (17) + +#define BUTTON_PIN 40 +// #define BUTTON_PIN 0 // This is the BOOT button pad at the moment +// #define BUTTON_NEED_PULLUP + +// #define USE_RF95 // RFM95/SX127x + +#undef LORA_SCK +#undef LORA_MISO +#undef LORA_MOSI +#undef LORA_CS + +// WaveShare Core1262-868M OK +// https://www.waveshare.com/wiki/Core1262-868M +#define USE_SX1262 + +#ifdef USE_SX1262 +#define LORA_MISO 39 +#define LORA_SCK 21 +#define LORA_MOSI 38 +#define LORA_CS 17 +#define LORA_RESET 42 +#define LORA_DIO1 5 +#define LORA_BUSY 47 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_BUSY +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif + +// #define USE_SX1280 +#ifdef USE_SX1280 +#define LORA_MISO 1 +#define LORA_SCK 3 +#define LORA_MOSI 4 +#define LORA_CS 2 +#define LORA_RESET 17 +#define LORA_DIO1 12 +#define LORA_BUSY 47 +#define SX128X_CS LORA_CS +#define SX128X_DIO1 LORA_DIO1 +#define SX128X_BUSY LORA_BUSY +#define SX128X_RESET LORA_RESET +#endif + +// #define USE_EINK +/* + * eink display pins + */ +// #define PIN_EINK_CS +// #define PIN_EINK_BUSY +// #define PIN_EINK_DC +// #define PIN_EINK_RES (-1) +// #define PIN_EINK_SCLK 3 +// #define PIN_EINK_MOSI 4 diff --git a/variants/canaryone/platformio.ini b/variants/canaryone/platformio.ini new file mode 100644 index 0000000..5e01c37 --- /dev/null +++ b/variants/canaryone/platformio.ini @@ -0,0 +1,14 @@ +; Public Beta oled/nrf52840/sx1262 device +[env:canaryone] +extends = nrf52840_base +board = canaryone +debug_tool = jlink + +# add -DCFG_SYSVIEW if you want to use the Segger systemview tool for OS profiling. +build_flags = ${nrf52840_base.build_flags} -Ivariants/canaryone + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/canaryone> +lib_deps = + ${nrf52840_base.lib_deps} + lewisxhe/PCF8563_Library@^1.0.1 +;upload_protocol = fs diff --git a/variants/canaryone/variant.cpp b/variants/canaryone/variant.cpp new file mode 100644 index 0000000..5967a2a --- /dev/null +++ b/variants/canaryone/variant.cpp @@ -0,0 +1,56 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 - pins 0 and 1 are hardwired for xtal and should never be enabled + 0xff, 0xff, 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, + + // 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); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + + 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); + + // 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); +} diff --git a/variants/canaryone/variant.h b/variants/canaryone/variant.h new file mode 100644 index 0000000..140b605 --- /dev/null +++ b/variants/canaryone/variant.h @@ -0,0 +1,181 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_CANARYONE +#define _VARIANT_CANARYONE + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +#define CANARYONE + +#define GPIO_PORT0 0 +#define GPIO_PORT1 32 + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (GPIO_PORT1 + 1) // blue P1.01 +#define PIN_LED2 (GPIO_PORT0 + 14) // yellow P0.14 +#define PIN_LED3 (GPIO_PORT1 + 3) // green P1.03 + +#define LED_BLUE PIN_LED1 + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED3 + +#define LED_STATE_ON 0 // State when LED is lit + +/* + * Buttons + */ +#define PIN_BUTTON1 (GPIO_PORT0 + 15) // BTN0 on schematic +#define PIN_BUTTON2 (GPIO_PORT0 + 16) // BTN1 on schematic + +/* + * Analog pins + */ +#define PIN_A0 (4) // Battery ADC P0.04 + +#define BATTERY_PIN PIN_A0 + +static const uint8_t A0 = PIN_A0; + +#define ADC_RESOLUTION 14 + +/** + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (GPIO_PORT0 + 26) +// #define I2C_SDA (GPIO_PORT0 + 26) +#define PIN_WIRE_SCL (GPIO_PORT0 + 27) +// #define I2C_SCL (GPIO_PORT0 + 27) + +#define PIN_LCD_RESET (GPIO_PORT0 + 2) + +/* + * External serial flash WP25R1635FZUIL0 + */ + +// QSPI Pins +#define PIN_QSPI_SCK (GPIO_PORT1 + 14) +#define PIN_QSPI_CS (GPIO_PORT1 + 15) +#define PIN_QSPI_IO0 (GPIO_PORT1 + 12) // MOSI if using two bit interface +#define PIN_QSPI_IO1 (GPIO_PORT1 + 13) // MISO if using two bit interface +#define PIN_QSPI_IO2 (GPIO_PORT0 + 7) // WP if using two bit interface (i.e. not used) +#define PIN_QSPI_IO3 (GPIO_PORT0 + 5) // HOLD if using two bit interface (i.e. not used) + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES MX25R1635F +#define EXTERNAL_FLASH_USE_QSPI + +// Add a delay on startup to allow LoRa and GPS to power up +#define PIN_PWR_DELAY_MS 100 + +/* + * Lora radio + */ +#define RADIOLIB_DEBUG 1 +#define USE_SX1262 +#define SX126X_CS (GPIO_PORT0 + 24) +#define SX126X_DIO1 (GPIO_PORT1 + 11) +// #define SX126X_DIO3 (GPIO_PORT0 + 21) +// #define SX126X_DIO2 () // LORA_BUSY // LoRa RX/TX +#define SX126X_BUSY (GPIO_PORT0 + 17) +#define SX126X_RESET (GPIO_PORT0 + 25) +#define LORA_RF_PWR (GPIO_PORT0 + 28) // LORA_RF_SWITCH + +/* + * GPS pins + */ +#define HAS_GPS 1 +#define GPS_UBLOX +#define GPS_BAUDRATE 38400 + +// #define PIN_GPS_WAKE (GPIO_PORT1 + 2) // An output to wake GPS, low means allow sleep, high means force wake +// Seems to be missing on this new board +#define PIN_GPS_PPS (GPIO_PORT1 + 4) // Pulse per second input from the GPS +#define GPS_TX_PIN (GPIO_PORT1 + 9) // This is for bits going TOWARDS the CPU +#define GPS_RX_PIN (GPIO_PORT1 + 8) // This is for bits going TOWARDS the GPS + +#define GPS_THREAD_INTERVAL 50 + +#define PIN_SERIAL1_RX GPS_TX_PIN +#define PIN_SERIAL1_TX GPS_RX_PIN + +#define GPS_RESET_PIN (GPIO_PORT1 + 5) // GPS reset pin + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 1 + +// For LORA, spi 0 +#define PIN_SPI_MISO (GPIO_PORT0 + 23) +#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, +// 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) + +// To debug via the segger JLINK console rather than the CDC-ACM serial device +#define USE_SEGGER 1 + +// #define LORA_DISABLE_SENDING 1 +#define SX126X_DIO2_AS_RF_SWITCH 1 + +// Battery +// The battery sense is hooked to pin A0 (4) +// it is defined in the anlaolgue pin section of this file +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER (2.0F) + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif \ No newline at end of file diff --git a/variants/chatter2/platformio.ini b/variants/chatter2/platformio.ini new file mode 100644 index 0000000..1f086cf --- /dev/null +++ b/variants/chatter2/platformio.ini @@ -0,0 +1,12 @@ +; CircuitMess Chatter 2 based on ESP32-WROOM-32 (38 pins) devkit & DeeamLNK DL-LLCC68 or Heltec HT RA62 SX1262/SX1268 module +[env:chatter2] +extends = esp32_base +board = esp32doit-devkit-v1 +build_flags = + ${esp32_base.build_flags} + -D CHATTER_2 + -I variants/chatter2 + +lib_deps = + ${esp32_base.lib_deps} + lovyan03/LovyanGFX@^1.1.8 \ No newline at end of file diff --git a/variants/chatter2/variant.h b/variants/chatter2/variant.h new file mode 100644 index 0000000..5c27e2f --- /dev/null +++ b/variants/chatter2/variant.h @@ -0,0 +1,125 @@ +////////////////////////////////////////////////////////////////////////////////// +// // +// Have custom connections or functionality? Configure them in this section // +// // +////////////////////////////////////////////////////////////////////////////////// + +// Debugging +// #define GPS_DEBUG +// #define GPS_EXTRAVERBOSE + +// Lora +#define USE_LLCC68 // Original Chatter2 with LLCC68 module +#define USE_SX1262 // Added for when Lora module is swapped for HT-RA62 + +#define SX126X_CS 14 // module's NSS pin +#define LORA_SCK 16 // module's SCK pin +#define LORA_MOSI 5 // module's MOSI pin +#define LORA_MISO 17 // module's MISO pin +#define SX126X_RESET RADIOLIB_NC // module's NRST pin +#define SX126X_BUSY 4 // module's BUSY pin works for both LLCC68 and RA-62 with cut & jumper +#define SX126X_DIO1 18 // module's DIO1 pin +#define SX126X_DIO2_AS_RF_SWITCH // module's DIO2 pin +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 // module's DIO pin +#define SX126X_TXEN RADIOLIB_NC +#define SX126X_RXEN RADIOLIB_NC + +// 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) + +// Buzzer +#define PIN_BUZZER 19 +// Buttons +// #define BUTTON_PIN 36 // Use the WAKE button as the user button +// I2C +// #define I2C_SCL 27 +// #define I2C_SDA 26 + +#define SX126X_MAX_POWER 22 // SX126xInterface.cpp defaults to 22 if not defined, but here we define it for good practice + +// Display + +#define HAS_SCREEN 1 // Assume no screen present by default to prevent crash... + +// ST7735S TFT LCD +#define ST7735S 1 // there are different (sub-)versions of ST7735 +#define ST7735_CS -1 +#define ST7735_RS 33 // DC +#define ST7735_SDA 26 // MOSI +#define ST7735_SCK 27 +#define ST7735_RESET 15 +#define ST7735_MISO -1 +#define ST7735_BUSY -1 +#define TFT_BL 32 +#define ST7735_SPI_HOST HSPI_HOST // SPI2_HOST for S3, auto may work too +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 16000000 +#define TFT_HEIGHT 160 +#define TFT_WIDTH 128 +#define TFT_OFFSET_X 0 +#define TFT_OFFSET_Y 0 +#define TFT_INVERT false +#define SCREEN_ROTATE +#define SCREEN_TRANSITION_FRAMERATE 5 // fps +#define DISPLAY_FORCE_SMALL_FONTS +#define TFT_BACKLIGHT_ON LOW + +// Battery + +#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 + // ESP32-S2/C3/S3 are different + // lower dB for lower voltage rnage +#define ADC_MULTIPLIER 5.0 // VBATT---10k--pin34---2.5K---GND +// Chatter2 uses 3 AAA cells +#define CELL_TYPE_ALKALINE +#define NUM_CELLS 3 +#undef EXT_PWR_DETECT + +// GPS +// FIXME: unsure what to define HAS_GPS as if GPS isn't always present +#define HAS_GPS 1 // Don't need to set this to 0 to prevent a crash as it doesn't crash if GPS not found, will probe by default +// #define PIN_GPS_EN 15 +// #define GPS_EN_ACTIVE 1 +#undef GPS_TX_PIN +#undef GPS_RX_PIN +#define GPS_TX_PIN 13 +#define GPS_RX_PIN 2 + +// keyboard +#define INPUTBROKER_SERIAL_TYPE 1 +#define KB_LOAD 21 // load values from the switch and store in shift register +#define KB_CLK 22 // clock pin for serial data out +#define KB_DATA 23 // data pin +#define CANNED_MESSAGE_MODULE_ENABLE 1 + +///////////////////////////////////////////////////////////////////////////////// +// // +// You should have no need to modify the code below, nor in pins_arduino.h // +// // +///////////////////////////////////////////////////////////////////////////////// + +#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.) diff --git a/variants/diy/dr-dev/variant.h b/variants/diy/dr-dev/variant.h new file mode 100644 index 0000000..35b18ee --- /dev/null +++ b/variants/diy/dr-dev/variant.h @@ -0,0 +1,76 @@ +// Initialize i2c bus on sd_dat and esp_led pins, respectively. We need a bus to not hang on boot +#define HAS_SCREEN 0 +#define I2C_SDA 4 +#define I2C_SCL 5 +#define BATTERY_PIN 34 +#define ADC_CHANNEL ADC1_GPIO34_CHANNEL + +// GPS +#undef GPS_RX_PIN +#define GPS_RX_PIN NOT_A_PIN +#define HAS_GPS 0 + +#define BUTTON_PIN 13 // The middle button GPIO on the T-Beam +#define BUTTON_NEED_PULLUP +#define EXT_NOTIFY_OUT 12 // Overridden default pin to use for Ext Notify Module (#975). + +#define LORA_DIO0 NOT_A_PIN // a No connect on the SX1262/SX1268 module +#define LORA_RESET NOT_A_PIN // RST for SX1276, and for SX1262/SX1268 +#define LORA_DIO3 NOT_A_PIN // Not connected on PCB, but internally on the SX1262/SX1268, if DIO3 is high the TXCO is enabled + +// In transmitting, set TXEN as high communication level,RXEN pin is low level; +// In receiving, set RXEN as high communication level, TXEN is lowlevel; +// Before powering off, set TXEN、RXEN as low level. + +#undef LORA_SCK +#define LORA_SCK 18 +#undef LORA_MISO +#define LORA_MISO 19 +#undef LORA_MOSI +#define LORA_MOSI 23 + +// PINS FOR THE 900M22S + +#define LORA_DIO1 26 // IRQ for SX1262/SX1268 +#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 +#undef LORA_CS +#define LORA_CS 16 +#define SX126X_BUSY 22 +#define SX126X_CS 16 + +// PINS FOR THE 900M30S +/* +#define LORA_DIO1 27 // IRQ for SX1262/SX1268 +#define LORA_DIO2 35 // BUSY for SX1262/SX1268 +#define LORA_TXEN NOT_A_PIN // Input - RF switch TX control, connecting external MCU IO or DIO2, valid in high level +#define LORA_RXEN 21 // Input - RF switch RX control, connecting external MCU IO, valid in high level +#undef LORA_CS +#define LORA_CS 33 +#define SX126X_BUSY 35 +#define SX126X_CS 33 +*/ + +// RX/TX for RFM95/SX127x +// #define RF95_RXEN LORA_RXEN +#define RF95_TXEN LORA_TXEN +// #define RF95_TCXO + +// common pinouts for SX126X modules + +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_RESET LORA_RESET +// #define SX126X_RXEN LORA_RXEN +#define SX126X_TXEN LORA_TXEN + +// supported modules list +// #define USE_RF95 // RFM95/SX127x +#define USE_SX1262 +// #define USE_SX1268 +// #define USE_LLCC68 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 diff --git a/variants/diy/hydra/variant.h b/variants/diy/hydra/variant.h new file mode 100644 index 0000000..60bb60b --- /dev/null +++ b/variants/diy/hydra/variant.h @@ -0,0 +1,41 @@ +// For OLED LCD +#define I2C_SDA 21 +#define I2C_SCL 22 + +// For GPS, 'undef's not needed +#define GPS_TX_PIN 15 +#define GPS_RX_PIN 12 +#define PIN_GPS_EN 4 +#define GPS_POWER_TOGGLE // Moved definition from platformio.ini to here + +#define BUTTON_PIN 39 // The middle button GPIO on the T-Beam +#define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +#define ADC_CHANNEL ADC1_GPIO35_CHANNEL +#define ADC_MULTIPLIER 1.85 // (R1 = 470k, R2 = 680k) +#define EXT_PWR_DETECT 4 // Pin to detect connected external power source for LILYGO® TTGO T-Energy T18 and other DIY boards +#define EXT_NOTIFY_OUT 12 // Overridden default pin to use for Ext Notify Module (#975). +#define LED_PIN 2 // add status LED (compatible with core-pcb and DIY targets) + +// 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_DIO3_TCXO_VOLTAGE 1.8 // E22 series TCXO reference voltage is 1.8V + +#define SX126X_CS 18 // EBYTE module's NSS pin +#define SX126X_SCK 5 // EBYTE module's SCK pin +#define SX126X_MOSI 27 // EBYTE module's MOSI pin +#define SX126X_MISO 19 // EBYTE module's MISO pin +#define SX126X_RESET 23 // EBYTE module's NRST pin +#define SX126X_BUSY 32 // EBYTE module's BUSY pin +#define SX126X_DIO1 33 // EBYTE module's DIO1 pin + +#define SX126X_TXEN 13 // Schematic connects EBYTE module's TXEN pin to MCU +#define SX126X_RXEN 14 // Schematic connects EBYTE module's RXEN pin to MCU + +#define LORA_CS SX126X_CS // Compatibility with variant file configuration structure +#define LORA_SCK SX126X_SCK // Compatibility with variant file configuration structure +#define LORA_MOSI SX126X_MOSI // Compatibility with variant file configuration structure +#define LORA_MISO SX126X_MISO // Compatibility with variant file configuration structure +#define LORA_DIO1 SX126X_DIO1 // Compatibility with variant file configuration structure diff --git a/variants/diy/mesh-tab/variant.h b/variants/diy/mesh-tab/variant.h new file mode 100644 index 0000000..0a23a3c --- /dev/null +++ b/variants/diy/mesh-tab/variant.h @@ -0,0 +1,49 @@ +#ifndef _VARIANT_MESHTAB_DIY_ +#define _VARIANT_MESHTAB_DIY_ + +#define HAS_TOUCHSCREEN 1 + +#define SLEEP_TIME 120 + +// Analog pins +#define BATTERY_PIN 4 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +// ratio of voltage divider = 2.0 +#define ADC_MULTIPLIER 2.11 // 2.0 + 10% for correction of display undervoltage. +#define ADC_CHANNEL ADC1_GPIO4_CHANNEL + +// LED +#define LED_PIN 21 + +// Button +#define BUTTON_PIN 0 + +// GPS +#define GPS_RX_PIN 18 +#define GPS_TX_PIN 17 + +// #define HAS_SDCARD 1 +#define SPI_MOSI 13 +#define SPI_SCK 12 +#define SPI_MISO 11 +#define SPI_CS 10 +#define SDCARD_CS 6 + +// LORA SPI +#define LORA_SCK 36 +#define LORA_MISO 37 +#define LORA_MOSI 35 +#define LORA_CS 39 + +// LORA MODULES +#define USE_SX1262 + +// LORA CONFIG +#define SX126X_CS LORA_CS +#define SX126X_DIO1 15 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_BUSY 40 +#define SX126X_RESET 14 +#define SX126X_RXEN 47 +#define SX126X_TXEN RADIOLIB_NC // Assuming that DIO2 is connected to TXEN pin + +#endif diff --git a/variants/diy/nrf52_promicro_diy_tcxo/variant.cpp b/variants/diy/nrf52_promicro_diy_tcxo/variant.cpp new file mode 100644 index 0000000..5869ed1 --- /dev/null +++ b/variants/diy/nrf52_promicro_diy_tcxo/variant.cpp @@ -0,0 +1,38 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 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, + + // 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); +} diff --git a/variants/diy/nrf52_promicro_diy_tcxo/variant.h b/variants/diy/nrf52_promicro_diy_tcxo/variant.h new file mode 100644 index 0000000..5c535ba --- /dev/null +++ b/variants/diy/nrf52_promicro_diy_tcxo/variant.h @@ -0,0 +1,182 @@ +#ifndef _VARIANT_PROMICRO_DIY_ +#define _VARIANT_PROMICRO_DIY_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +// #define USE_LFXO // Board uses 32khz crystal for LF +#define USE_LFRC // Board uses RC for LF + +#define PROMICRO_DIY_TCXO + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +/* +NRF52 PRO MICRO PIN ASSIGNMENT + +| Pin   | Function   |   | Pin     | Function     | RF95 | +| ----- | ----------- | --- | -------- | ------------ | ----- | +| Gnd   |             |   | vbat     |             | | +| P0.06 | Serial2 RX |   | vbat     |             | | +| P0.08 | Serial2 TX |   | Gnd     |             | | +| Gnd   |             |   | reset   |             | | +| Gnd   |             |   | ext_vcc | *see 0.13   | | +| P0.17 | RXEN       |   | P0.31   | BATTERY_PIN | | +| P0.20 | GPS_RX     |   | P0.29   | BUSY         | DIO0 | +| P0.22 | GPS_TX     |   | P0.02   | MISO | MISO | +| P0.24 | GPS_EN     |   | P1.15   | MOSI         | MOSI | +| P1.00 | BUTTON_PIN |   | P1.13   | CS           | CS   | +| P0.11 | SCL         |   | P1.11   | SCK         | SCK | +| P1.04 | SDA         |   | P0.10   | DIO1/IRQ     | DIO1 | +| P1.06 | Free pin   |   | P0.09   | RESET       | RST | +|       |             |   |         |             | | +|       | Mid board   |   |         | Internal     | | +| P1.01 | Free pin   |   | 0.15     | LED         | | +| P1.02 | Free pin   |   | 0.13     | 3V3_EN       | | +| P1.07 | Free pin   |   |         |             | | +*/ + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +// Pin 13 enables 3.3V periphery. If the Lora module is on this pin, then it should stay enabled at all times. +#define PIN_3V3_EN (0 + 13) // P0.13 + +// Analog pins +#define BATTERY_PIN (0 + 31) // P0.31 Battery ADC +#define ADC_CHANNEL ADC1_GPIO4_CHANNEL +#define ADC_RESOLUTION 14 +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +// Definition of milliVolt per LSB => 3.0V ADC range and 12-bit ADC resolution = 3000mV/4096 +#define VBAT_MV_PER_LSB (0.73242188F) +// Voltage divider value => 1.5M + 1M voltage divider on VBAT = (1.5M / (1M + 1.5M)) +#define VBAT_DIVIDER (0.6F) +// Compensation factor for the VBAT divider +#define VBAT_DIVIDER_COMP (1.73) +// Fixed calculation of milliVolt from compensation value +#define REAL_VBAT_MV_PER_LSB (VBAT_DIVIDER_COMP * VBAT_MV_PER_LSB) +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER VBAT_DIVIDER_COMP // REAL_VBAT_MV_PER_LSB +#define VBAT_RAW_TO_SCALED(x) (REAL_VBAT_MV_PER_LSB * x) + +// WIRE IC AND IIC PINS +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (32 + 4) // P1.04 +#define PIN_WIRE_SCL (0 + 11) // P0.11 + +// LED +#define PIN_LED1 (0 + 15) // P0.15 +#define LED_BUILTIN PIN_LED1 +// Actually red +#define LED_BLUE PIN_LED1 +#define LED_STATE_ON 1 // State when LED is lit + +// Button +#define BUTTON_PIN (32 + 0) // P1.00 + +// GPS +#define PIN_GPS_TX (0 + 22) // P0.22 +#define PIN_GPS_RX (0 + 20) // P0.20 + +#define PIN_GPS_EN (0 + 24) // P0.24 +#define GPS_POWER_TOGGLE +#define GPS_UBLOX +// define GPS_DEBUG + +// UART interfaces +#define PIN_SERIAL1_RX PIN_GPS_TX +#define PIN_SERIAL1_TX PIN_GPS_RX + +#define PIN_SERIAL2_RX (0 + 6) // P0.06 +#define PIN_SERIAL2_TX (0 + 8) // P0.08 + +// Serial interfaces +#define SPI_INTERFACES_COUNT 1 + +#define PIN_SPI_MISO (0 + 2) // P0.02 +#define PIN_SPI_MOSI (32 + 15) // P1.15 +#define PIN_SPI_SCK (32 + 11) // P1.11 + +#define LORA_MISO PIN_SPI_MISO +#define LORA_MOSI PIN_SPI_MOSI +#define LORA_SCK PIN_SPI_SCK +#define LORA_CS (32 + 13) // P1.13 + +// LORA MODULES +#define USE_LLCC68 +#define USE_SX1262 +#define USE_RF95 +#define USE_SX1268 + +// RF95 CONFIG + +#define LORA_DIO0 (0 + 29) // P0.10 IRQ +#define LORA_DIO1 (0 + 10) // P0.10 IRQ +#define LORA_RESET (0 + 9) // P0.09 + +// RX/TX for RFM95/SX127x +#define RF95_RXEN (0 + 17) // P0.17 +#define RF95_TXEN RADIOLIB_NC // Assuming that DIO2 is connected to TXEN pin. If not, TXEN must be connected. + +// 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_BUSY (0 + 29) // P0.29 +#define SX126X_RESET (0 + 9) // P0.09 +#define SX126X_RXEN (0 + 17) // P0.17 +#define SX126X_TXEN RADIOLIB_NC // Assuming that DIO2 is connected to TXEN pin. If not, TXEN must be connected. + +// #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. + +| Mfr | Module | TCXO | RF Switch | Notes | +| ------------ | ---------------- | ---- | --------- | ------------------------------------- | +| Ebyte | E22-900M22S | Yes | Ext | | +| Ebyte | E22-900MM22S | No | Ext | | +| Ebyte | E22-900M30S | Yes | Ext | | +| Ebyte | E22-900M33S | Yes | Ext | MAX_POWER must be set to 8 for this | +| Ebyte | E220-900M22S | No | Ext | LLCC68, looks like DIO3 not connected | +| AI-Thinker | RA-01SH | No | Int | SX1262 | +| Heltec | HT-RA62 | Yes | Int | | +| NiceRF | Lora1262 | yes | Int | | +| Waveshare | Core1262-HF | yes | Ext | | +| Waveshare | LoRa Node Module | yes | Int | | +| Seeed | Wio-SX1262 | yes | Int | Sooooo cute! | +| AI-Thinker | RA-02 | No | Int | SX1278 **433mhz band only** | +| RF Solutions | RFM95 | No | Int | Untested | + +*/ + +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#define TCXO_OPTIONAL // make it so that the firmware can try both TCXO and XTAL +extern float tcxoVoltage; // make this available everywhere + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif diff --git a/variants/diy/nrf52_promicro_diy_xtal/variant.cpp b/variants/diy/nrf52_promicro_diy_xtal/variant.cpp new file mode 100644 index 0000000..5869ed1 --- /dev/null +++ b/variants/diy/nrf52_promicro_diy_xtal/variant.cpp @@ -0,0 +1,38 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 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, + + // 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); +} diff --git a/variants/diy/nrf52_promicro_diy_xtal/variant.h b/variants/diy/nrf52_promicro_diy_xtal/variant.h new file mode 100644 index 0000000..7aafab7 --- /dev/null +++ b/variants/diy/nrf52_promicro_diy_xtal/variant.h @@ -0,0 +1,155 @@ +#ifndef _VARIANT_PROMICRO_DIY_ +#define _VARIANT_PROMICRO_DIY_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +// #define USE_LFXO // Board uses 32khz crystal for LF +#define USE_LFRC // Board uses RC for LF + +#define PROMICRO_DIY_XTAL +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +/* +NRF52 PRO MICRO PIN ASSIGNMENT + +| Pin | Function | | Pin | Function | +|-------|------------|---|---------|-------------| +| Gnd | | | vbat | | +| P0.06 | Serial2 RX | | vbat | | +| P0.08 | Serial2 TX | | Gnd | | +| Gnd | | | reset | | +| Gnd | | | ext_vcc | *see 0.13 | +| P0.17 | RXEN | | P0.31 | BATTERY_PIN | +| P0.20 | GPS_RX | | P0.29 | BUSY | +| P0.22 | GPS_TX | | P0.02 | MISO | +| P0.24 | GPS_EN | | P1.15 | MOSI | +| P1.00 | BUTTON_PIN | | P1.13 | CS | +| P0.11 | SCL | | P1.11 | SCK | +| P1.04 | SDA | | P0.10 | DIO1/IRQ | +| P1.06 | Free pin | | P0.09 | RESET | +| | | | | | +| | Mid board | | | Internal | +| P1.01 | Free pin | | 0.15 | LED | +| P1.02 | Free pin | | 0.13 | 3V3_EN | +| P1.07 | Free pin | | | | +*/ + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +// Pin 13 enables 3.3V periphery. If the Lora module is on this pin, then it should stay enabled at all times. +#define PIN_3V3_EN (0 + 13) // P0.13 + +// Analog pins +#define BATTERY_PIN (0 + 31) // P0.31 Battery ADC +#define ADC_CHANNEL ADC1_GPIO4_CHANNEL +#define ADC_RESOLUTION 14 +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +// Definition of milliVolt per LSB => 3.0V ADC range and 12-bit ADC resolution = 3000mV/4096 +#define VBAT_MV_PER_LSB (0.73242188F) +// Voltage divider value => 1.5M + 1M voltage divider on VBAT = (1.5M / (1M + 1.5M)) +#define VBAT_DIVIDER (0.6F) +// Compensation factor for the VBAT divider +#define VBAT_DIVIDER_COMP (1.73) +// Fixed calculation of milliVolt from compensation value +#define REAL_VBAT_MV_PER_LSB (VBAT_DIVIDER_COMP * VBAT_MV_PER_LSB) +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER VBAT_DIVIDER_COMP // REAL_VBAT_MV_PER_LSB +#define VBAT_RAW_TO_SCALED(x) (REAL_VBAT_MV_PER_LSB * x) + +// WIRE IC AND IIC PINS +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (32 + 4) // P1.04 +#define PIN_WIRE_SCL (0 + 11) // P0.11 + +// LED +#define PIN_LED1 (0 + 15) // P0.15 +#define LED_BUILTIN PIN_LED1 +// Actually red +#define LED_BLUE PIN_LED1 +#define LED_STATE_ON 1 // State when LED is lit + +// Button +#define BUTTON_PIN (32 + 0) // P1.00 + +// GPS +#define PIN_GPS_TX (0 + 22) // P0.22 +#define PIN_GPS_RX (0 + 20) // P0.20 + +#define PIN_GPS_EN (0 + 24) // P0.24 +#define GPS_POWER_TOGGLE +#define GPS_UBLOX +// define GPS_DEBUG + +// UART interfaces +#define PIN_SERIAL1_RX PIN_GPS_TX +#define PIN_SERIAL1_TX PIN_GPS_RX + +#define PIN_SERIAL2_RX (0 + 6) // P0.06 +#define PIN_SERIAL2_TX (0 + 8) // P0.08 + +// Serial interfaces +#define SPI_INTERFACES_COUNT 1 + +#define PIN_SPI_MISO (0 + 2) // P0.02 +#define PIN_SPI_MOSI (32 + 15) // P1.15 +#define PIN_SPI_SCK (32 + 11) // P1.11 + +// LORA MODULES +#define USE_LLCC68 +#define USE_SX1262 +// #define USE_RF95 +#define USE_SX1268 + +// LORA 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_BUSY (0 + 29) // P0.29 +#define SX126X_RESET (0 + 9) // P0.09 +#define SX126X_RXEN (0 + 17) // P0.17 +#define SX126X_TXEN RADIOLIB_NC // Assuming that DIO2 is connected to TXEN pin. If not, TXEN must be connected. + +/* +On the SX1262, DIO3 sets the voltage for an external TCXO, if one is present. If one is not present, then this should not be used. + +Ebyte +e22-900mm22s has no TCXO +e22-900m22s has TCXO +e220-900mm22s has no TCXO, works with/without this definition, looks like DIO3 not connected at all + +AI-thinker +RA-01SH does not have TCXO + +Waveshare +Core1262 has TCXO + +*/ +// #define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif \ No newline at end of file diff --git a/variants/diy/platformio.ini b/variants/diy/platformio.ini new file mode 100644 index 0000000..00ff88d --- /dev/null +++ b/variants/diy/platformio.ini @@ -0,0 +1,160 @@ +; Meshtastic DIY v1 by Nano VHF Schematic based on ESP32-WROOM-32 (38 pins) devkit & EBYTE E22 SX1262/SX1268 module +[env:meshtastic-diy-v1] +extends = esp32_base +board = esp32doit-devkit-v1 +board_check = true +build_flags = + ${esp32_base.build_flags} + -D DIY_V1 + -D EBYTE_E22 + -I variants/diy/v1 + +; Meshtastic DIY v1.1 new schematic based on ESP32-WROOM-32 & SX1262/SX1268 modules +[env:meshtastic-diy-v1_1] +extends = esp32_base +board = esp32doit-devkit-v1 +board_level = extra +build_flags = + ${esp32_base.build_flags} + -D DIY_V1 + -D EBYTE_E22 + -I variants/diy/v1_1 + +; Port to Disaster Radio's ESP32-v3 Dev Board +[env:meshtastic-dr-dev] +extends = esp32_base +board = esp32doit-devkit-v1 +board_upload.maximum_size = 4194304 +board_upload.maximum_ram_size = 532480 +build_flags = + ${esp32_base.build_flags} + -D DR_DEV + -D EBYTE_E22 + -I variants/diy/dr-dev + +; Hydra - Meshtastic DIY v1 hardware with some specific changes +[env:hydra] +extends = esp32_base +board = esp32doit-devkit-v1 +build_flags = + ${esp32_base.build_flags} + -D DIY_V1 + -I variants/diy/hydra + + +; Promicro + E22(0)-xxxMM / RA-01SH modules board variant - DIY - without TCXO +[env:nrf52_promicro_diy_xtal] +extends = nrf52840_base +board = promicro-nrf52840 +board_level = extra +build_flags = ${nrf52840_base.build_flags} + -I variants/diy/nrf52_promicro_diy_xtal + -D NRF52_PROMICRO_DIY + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/diy/nrf52_promicro_diy_xtal> +lib_deps = + ${nrf52840_base.lib_deps} +debug_tool = jlink + + +; Promicro + E22(0)-xxxM / HT-RA62 modules board variant - DIY - with TCXO +[env:nrf52_promicro_diy_tcxo] +extends = nrf52840_base +board = promicro-nrf52840 +board_level = extra +build_flags = ${nrf52840_base.build_flags} + -I variants/diy/nrf52_promicro_diy_tcxo + -D NRF52_PROMICRO_DIY + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/diy/nrf52_promicro_diy_tcxo> +lib_deps = + ${nrf52840_base.lib_deps} +debug_tool = jlink + +; NanoVHF T-Energy-S3 + E22(0)-xxxM - DIY +[env:t-energy-s3_e22] +extends = esp32s3_base +board = esp32-s3-devkitc-1 +board_level = extra +board_upload.flash_size = 16MB ;Specify the FLASH capacity as 16MB +board_build.arduino.memory_type = qio_opi ;Enable internal PSRAM +build_unflags = + ${esp32s3_base.build_unflags} + -D ARDUINO_USB_MODE=1 +build_flags = + ${esp32s3_base.build_flags} + -D EBYTE_ESP32_S3 + -D BOARD_HAS_PSRAM + -D ARDUINO_USB_MODE=0 + -D ARDUINO_USB_CDC_ON_BOOT=1 + -I variants/diy/t-energy-s3_e22 + +; esp32-s3 + ra-sh01 lora + 3.2" ILI9143 +[env:mesh-tab] +extends = esp32s3_base +board = um_feathers3 +board_level = extra +board_upload.flash_size = 16MB +board_build.partitions = default_16MB.csv +upload_protocol = esptool +build_flags = ${esp32s3_base.build_flags} + -D MESH_TAB + -D PRIVATE_HW + -D CONFIG_ARDUHAL_ESP_LOG + -D CONFIG_ARDUHAL_LOG_COLORS=1 + -D CONFIG_DISABLE_HAL_LOCKS=1 ; "feels" to be a bit more stable without locks + -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 + -D MESHTASTIC_EXCLUDE_INPUTBROKER=1 + -D MESHTASTIC_EXCLUDE_BLUETOOTH=1 + -D MESHTASTIC_EXCLUDE_WEBSERVER=1 + -D LV_LVGL_H_INCLUDE_SIMPLE + -D LV_CONF_INCLUDE_SIMPLE + -D LV_COMP_CONF_INCLUDE_SIMPLE + -D LV_USE_SYSMON=0 + -D LV_USE_PROFILER=0 + -D LV_USE_PERF_MONITOR=0 + -D LV_USE_MEM_MONITOR=0 + -D LV_USE_LOG=0 + -D LV_BUILD_TEST=0 + -D USE_LOG_DEBUG + -D LOG_DEBUG_INC=\"DebugConfiguration.h\" + -D RADIOLIB_SPI_PARANOID=0 + -D MAX_NUM_NODES=250 + -D MAX_THREADS=40 + -D HAS_SCREEN=0 + -D HAS_TFT=1 + -D RAM_SIZE=1024 + -D LGFX_DRIVER_TEMPLATE + -D LGFX_DRIVER=LGFX_GENERIC + -D LGFX_PANEL=ILI9341 + -D LGFX_OFFSET_ROTATION=1 + -D LGFX_TOUCH=XPT2046 + -D LGFX_PIN_SCK=12 + -D LGFX_PIN_MOSI=13 + -D LGFX_PIN_MISO=11 + -D LGFX_PIN_DC=16 + -D LGFX_PIN_CS=10 + -D LGFX_PIN_RST=-1 + -D LGFX_PIN_BL=42 + -D LGFX_TOUCH_INT=41 + -D LGFX_TOUCH_CS=7 + -D LGFX_TOUCH_CLK=12 + -D LGFX_TOUCH_DO=11 + -D LGFX_TOUCH_DIN=13 + -D LGFX_TOUCH_X_MIN=300 + -D LGFX_TOUCH_X_MAX=3900 + -D LGFX_TOUCH_Y_MIN=400 + -D LGFX_TOUCH_Y_MAX=3900 + -D VIEW_320x240 + -D USE_PACKET_API + -I lib/device-ui/generated/ui_320x240 + -I variants/diy/mesh-tab +build_src_filter = ${esp32_base.build_src_filter} + +<../lib/device-ui/generated/ui_320x240> + +<../lib/device-ui/resources> + +<../lib/device-ui/locale> + +<../lib/device-ui/source> +lib_deps = ${esp32_base.lib_deps} + lovyan03/LovyanGFX@^1.1.16 + earlephilhower/ESP8266Audio@^1.9.7 + earlephilhower/ESP8266SAM@^1.0.1 diff --git a/variants/diy/t-energy-s3_e22/variant.h b/variants/diy/t-energy-s3_e22/variant.h new file mode 100644 index 0000000..6933d77 --- /dev/null +++ b/variants/diy/t-energy-s3_e22/variant.h @@ -0,0 +1,46 @@ +// NanoVHF T-Energy-S3 + E22(0)-xxxM - DIY +// https://github.com/NanoVHF/Meshtastic-DIY/tree/main/PCB/ESP-32-devkit_EBYTE-E22/Mesh-v1.06-TTGO-T18 + +// Battery +#define BATTERY_PIN 3 +#define ADC_MULTIPLIER 2.0 +#define ADC_CHANNEL ADC1_GPIO3_CHANNEL + +// Button on NanoVHF PCB +#define BUTTON_PIN 39 + +// I2C via connectors on NanoVHF PCB +#define I2C_SCL 2 +#define I2C_SDA 42 + +// Screen (disabled) +#define HAS_SCREEN 0 // Assume no screen present by default to prevent crash... + +// GPS via T-Energy-S3 onboard connector +#define HAS_GPS 1 +#define GPS_TX_PIN 43 +#define GPS_RX_PIN 44 + +// LoRa +#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 + +#define SX126X_MAX_POWER 22 // SX126xInterface.cpp defaults to 22 if not defined, but here we define it for good practice +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 // E22 series TCXO reference voltage is 1.8V + +#define SX126X_CS 5 // EBYTE module's NSS pin // FIXME: rename to SX126X_SS +#define SX126X_SCK 6 // EBYTE module's SCK pin +#define SX126X_MOSI 13 // EBYTE module's MOSI pin +#define SX126X_MISO 4 // EBYTE module's MISO pin +#define SX126X_RESET 1 // EBYTE module's NRST pin +#define SX126X_BUSY 48 // EBYTE module's BUSY pin +#define SX126X_DIO1 47 // EBYTE module's DIO1 pin + +#define SX126X_TXEN 10 // Schematic connects EBYTE module's TXEN pin to MCU +#define SX126X_RXEN 12 // Schematic connects EBYTE module's RXEN pin to MCU + +#define LORA_CS SX126X_CS // Compatibility with variant file configuration structure +#define LORA_SCK SX126X_SCK // Compatibility with variant file configuration structure +#define LORA_MOSI SX126X_MOSI // Compatibility with variant file configuration structure +#define LORA_MISO SX126X_MISO // Compatibility with variant file configuration structure +#define LORA_DIO1 SX126X_DIO1 // Compatibility with variant file configuration structure diff --git a/variants/diy/v1/variant.h b/variants/diy/v1/variant.h new file mode 100644 index 0000000..4802dbe --- /dev/null +++ b/variants/diy/v1/variant.h @@ -0,0 +1,56 @@ +// For OLED LCD +#define I2C_SDA 21 +#define I2C_SCL 22 + +// GPS +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define GPS_RX_PIN 12 +#define GPS_TX_PIN 15 +#define GPS_UBLOX + +#define BUTTON_PIN 39 // The middle button GPIO on the T-Beam +#define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +#define ADC_CHANNEL ADC1_GPIO35_CHANNEL +#define ADC_MULTIPLIER 1.85 // (R1 = 470k, R2 = 680k) +#define EXT_PWR_DETECT 4 // Pin to detect connected external power source for LILYGO® TTGO T-Energy T18 and other DIY boards +#define EXT_NOTIFY_OUT 12 // Overridden default pin to use for Ext Notify Module (#975). +#define LED_PIN 2 // add status LED (compatible with core-pcb and DIY targets) + +#define LORA_DIO0 26 // a No connect on the SX1262/SX1268 module +#define LORA_RESET 23 // RST for SX1276, and for SX1262/SX1268 +#define LORA_DIO1 33 // IRQ for SX1262/SX1268 +#define LORA_DIO2 32 // BUSY for SX1262/SX1268 +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262/SX1268, if DIO3 is high the TXCO is enabled + +#define LORA_SCK 5 +#define LORA_MISO 19 +#define LORA_MOSI 27 +#define LORA_CS 18 + +// supported modules list +#define USE_RF95 // RFM95/SX127x +#define USE_SX1262 +#define USE_SX1268 +#define USE_LLCC68 + +// common pinouts for SX126X modules +#define SX126X_CS 18 // NSS for SX126X +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_RXEN RADIOLIB_NC // Defining the RXEN ruins RFSwitching for the E22 900M30S in RadioLib +#define SX126X_TXEN 13 + +// RX/TX for RFM95/SX127x +#define RF95_RXEN 14 +#define RF95_TXEN 13 + +// Set lora.tx_power to 13 for Hydra or other E22 900M30S target due to PA +#define SX126X_MAX_POWER 22 + +#ifdef EBYTE_E22 +// Internally the TTGO module hooks the SX126x-DIO2 in to control the TX/RX switch +// (which is the default for the sx1262interface code) +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif diff --git a/variants/diy/v1_1/variant.h b/variants/diy/v1_1/variant.h new file mode 100644 index 0000000..8a006d0 --- /dev/null +++ b/variants/diy/v1_1/variant.h @@ -0,0 +1,57 @@ +// For OLED LCD +#define I2C_SDA 21 +#define I2C_SCL 22 + +// GPS +#undef GPS_RX_PIN +#define GPS_RX_PIN 15 + +#define BUTTON_PIN 2 // The middle button GPIO on the T-Beam +#define BUTTON_NEED_PULLUP +#define EXT_NOTIFY_OUT 12 // Overridden default pin to use for Ext Notify Module (#975). + +#define LORA_DIO0 26 // a No connect on the SX1262/SX1268 module +#define LORA_RESET 27 // RST for SX1276, and for SX1262/SX1268 +#define LORA_DIO1 33 // IRQ for SX1262/SX1268 +#define LORA_DIO2 32 // BUSY for SX1262/SX1268 +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262/SX1268, if DIO3 is high the TXCO is enabled + +// In transmitting, set TXEN as high communication level,RXEN pin is low level; +// In receiving, set RXEN as high communication level, TXEN is lowlevel; +// Before powering off, set TXEN、RXEN as low level. +#define LORA_RXEN 14 // Input - RF switch RX control, connecting external MCU IO, valid in high level +#define LORA_TXEN 13 // Input - RF switch TX control, connecting external MCU IO or DIO2, valid in high level + +#undef LORA_SCK +#define LORA_SCK 18 +#undef LORA_MISO +#define LORA_MISO 19 +#undef LORA_MOSI +#define LORA_MOSI 23 +#undef LORA_CS +#define LORA_CS 5 + +// RX/TX for RFM95/SX127x +#define RF95_RXEN LORA_RXEN +#define RF95_TXEN LORA_TXEN +// #define RF95_TCXO + +// common pinouts for SX126X modules +#define SX126X_CS 5 +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_RXEN LORA_RXEN +#define SX126X_TXEN LORA_TXEN + +// supported modules list +#define USE_RF95 // RFM95/SX127x +#define USE_SX1262 +#define USE_SX1268 +#define USE_LLCC68 + +#ifdef EBYTE_E22 +// Internally the TTGO module hooks the SX126x-DIO2 in to control the TX/RX switch +// (which is the default for the sx1262interface code) +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif diff --git a/variants/dreamcatcher/platformio.ini b/variants/dreamcatcher/platformio.ini new file mode 100644 index 0000000..46f9b98 --- /dev/null +++ b/variants/dreamcatcher/platformio.ini @@ -0,0 +1,27 @@ +[env:dreamcatcher] ; 2301, latest revision +extends = esp32s3_base +board = esp32s3box +board_level = extra + +build_flags = + ${esp32s3_base.build_flags} + -D PRIVATE_HW + -D OTHERNET_DC_REV=2301 + -I variants/dreamcatcher + -DARDUINO_USB_CDC_ON_BOOT=1 + +lib_deps = ${esp32s3_base.lib_deps} + earlephilhower/ESP8266Audio@^1.9.7 + earlephilhower/ESP8266SAM@^1.0.1 + +[env:dreamcatcher-2206] +extends = esp32s3_base +board = esp32s3box +board_level = extra + +build_flags = + ${esp32s3_base.build_flags} + -D PRIVATE_HW + -D OTHERNET_DC_REV=2206 + -I variants/dreamcatcher + -DARDUINO_USB_CDC_ON_BOOT=1 \ No newline at end of file diff --git a/variants/dreamcatcher/rfswitch.h b/variants/dreamcatcher/rfswitch.h new file mode 100644 index 0000000..74edb25 --- /dev/null +++ b/variants/dreamcatcher/rfswitch.h @@ -0,0 +1,17 @@ +#include "RadioLib.h" + +// RF Switch Matrix SubG RFO_HP_LF / RFO_LP_LF / RFI_[NP]_LF0 +// DIO5 -> RFSW0_V1 +// 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 Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 DIO7 + {LR11x0::MODE_STBY, {LOW, LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW, LOW}}, + {LR11x0::MODE_TX, {LOW, HIGH, LOW}}, {LR11x0::MODE_TX_HP, {LOW, HIGH, LOW}}, + {LR11x0::MODE_TX_HF, {LOW, LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW, HIGH}}, + {LR11x0::MODE_WIFI, {LOW, LOW, LOW}}, END_OF_MODE_TABLE, +}; \ No newline at end of file diff --git a/variants/dreamcatcher/variant.h b/variants/dreamcatcher/variant.h new file mode 100644 index 0000000..eb95a95 --- /dev/null +++ b/variants/dreamcatcher/variant.h @@ -0,0 +1,109 @@ +#undef I2C_SDA +#undef I2C_SCL +#define I2C_SDA 16 // I2C pins for this board +#define I2C_SCL 17 + +#define I2C_SDA1 45 +#define I2C_SCL1 46 + +#define LED_PIN 6 +#define LED_STATE_ON 1 +#define BUTTON_PIN 0 + +#define HAS_TPS65233 + +// V1 of SubG Switch SMA 0 or F Selector 1 +// #define RF_SW_SUBG1 8 +// V2 of SubG Switch SMA 1 or F Selector 0 +// #define RF_SW_SUBG2 5 + +#define RESET_OLED 8 // Emulate RF_SW_SUBG1, Use F Connector +#define VTFT_CTRL 5 // Emulate RF_SW_SUBG2, for SMA swap the pin values + +#if OTHERNET_DC_REV == 2206 +#define USE_LR1120 + +#define SPI_MISO 37 +#define SPI_MOSI 39 +#define SPI_SCK 38 +#define SDCARD_CS 40 + +#define PIN_BUZZER 48 + +// These can either be used for GPS or a serial link. Define through Protobufs +// #define GPS_RX_PIN 10 +// #define GPS_TX_PIN 21 + +#define PIN_POWER_EN 7 // RF section power supply enable +#define PERIPHERAL_WARMUP_MS 1000 // wait for TPS chip to initialize +#define TPS_EXTM 45 // connected, but not used +#define BIAS_T_ENABLE 9 // needs to be low +#define BIAS_T_VALUE 0 +#else // 2301 +#define USE_LR1121 +#define SPI_MISO 10 +#define SPI_MOSI 39 +#define SPI_SCK 38 + +#define SDCARD_CS 40 + +// This is only informational, we always use SD cards in 1 bit mode +#define SPI_DATA1 15 +#define SPI_DATA2 18 + +// These can either be used for GPS or a serial link. Define through Protobufs +// #define GPS_RX_PIN 36 +// #define GPS_TX_PIN 37 + +// dac / amp instead of buzzer +#define HAS_I2S +#define DAC_I2S_BCK 21 +#define DAC_I2S_WS 9 +#define DAC_I2S_DOUT 48 + +#define BIAS_T_ENABLE 7 // needs to be low +#define BIAS_T_VALUE 0 +#define BIAS_T_SUBGHZ 2 // also needs to be low, we hijack SENSOR_POWER_CTRL_PIN to emulate this +#define SENSOR_POWER_CTRL_PIN BIAS_T_SUBGHZ +#define SENSOR_POWER_ON 0 +#endif + +#define HAS_SDCARD // Have SPI interface SD card slot +#define SDCARD_USE_SPI1 + +#define LORA_RESET 3 +#define LORA_SCK 12 +#define LORA_MISO 13 +#define LORA_MOSI 11 +#define LORA_CS 14 +#define LORA_DIO9 4 +#define LORA_DIO2 47 + +#define LR1120_IRQ_PIN LORA_DIO9 +#define LR1120_NRESET_PIN LORA_RESET +#define LR1120_BUSY_PIN LORA_DIO2 +#define LR1120_SPI_NSS_PIN LORA_CS +#define LR1120_SPI_SCK_PIN LORA_SCK +#define LR1120_SPI_MOSI_PIN LORA_MOSI +#define LR1120_SPI_MISO_PIN LORA_MISO + +#define LR1121_IRQ_PIN LORA_DIO9 +#define LR1121_NRESET_PIN LORA_RESET +#define LR1121_BUSY_PIN LORA_DIO2 +#define LR1121_SPI_NSS_PIN LORA_CS +#define LR1121_SPI_SCK_PIN LORA_SCK +#define LR1121_SPI_MOSI_PIN LORA_MOSI +#define LR1121_SPI_MISO_PIN LORA_MISO + +#define LR11X0_DIO3_TCXO_VOLTAGE 1.8 +#define LR11X0_DIO_AS_RF_SWITCH + +// This board needs external switching between sub-GHz and 2.4G circuits + +// V1 of RF1 selector SubG 1 or 2.4GHz 0 +// #define RF_SW_SMA1 42 +// V2 of RF1 Selector SubG 0 or 2.4GHz 1 +// #define RF_SW_SMA2 41 + +#define LR11X0_RF_SWITCH_SUBGHZ 42 +#define LR11X0_RF_SWITCH_2_4GHZ 41 diff --git a/variants/esp32-s3-pico/pins_arduino.h b/variants/esp32-s3-pico/pins_arduino.h new file mode 100644 index 0000000..57a66fe --- /dev/null +++ b/variants/esp32-s3-pico/pins_arduino.h @@ -0,0 +1,28 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// The default Wire will be mapped to PMU and RTC +static const uint8_t SDA = 15; +static const uint8_t SCL = 16; + +// Default SPI will be mapped to Radio +static const uint8_t MISO = 37; +static const uint8_t SCK = 35; +static const uint8_t MOSI = 36; +static const uint8_t SS = 14; + +static const uint8_t BAT_ADC_PIN = 26; + +// #define SPI_MOSI (11) +// #define SPI_SCK (14) +// #define SPI_MISO (2) +// #define SPI_CS (13) + +// #define SDCARD_CS SPI_CS + +#endif /* Pins_Arduino_h */ \ No newline at end of file diff --git a/variants/esp32-s3-pico/platformio.ini b/variants/esp32-s3-pico/platformio.ini new file mode 100644 index 0000000..916f623 --- /dev/null +++ b/variants/esp32-s3-pico/platformio.ini @@ -0,0 +1,25 @@ +[env:ESP32-S3-Pico] + +board_level = extra +extends = esp32s3_base +upload_protocol = esptool +board = esp32-s3-pico + +board_upload.use_1200bps_touch = yes +board_upload.wait_for_upload_port = yes +board_upload.require_upload_port = yes + +;upload_port = /dev/ttyACM0 + +build_flags = ${esp32s3_base.build_flags} + -DESP32_S3_PICO + ;-DPRIVATE_HW + -Ivariants/esp32-s3-pico + -DBOARD_HAS_PSRAM + -DEINK_DISPLAY_MODEL=GxEPD2_290_T94_V2 + -DEINK_WIDTH=296 + -DEINK_HEIGHT=128 + +lib_deps = ${esp32s3_base.lib_deps} + zinggjm/GxEPD2@^1.5.3 + adafruit/Adafruit NeoPixel @ ^1.12.0 diff --git a/variants/esp32-s3-pico/variant.h b/variants/esp32-s3-pico/variant.h new file mode 100644 index 0000000..bfcb605 --- /dev/null +++ b/variants/esp32-s3-pico/variant.h @@ -0,0 +1,81 @@ +/* + +*/ +#define HAS_GPS 0 +#undef GPS_RX_PIN +#undef GPS_TX_PIN + +#define EXT_NOTIFY_OUT 22 +#define BUTTON_PIN 0 // 17 + +// #define LED_PIN PIN_LED +// Board has RGB LED 21 +#define HAS_NEOPIXEL // Enable the use of neopixels +#define NEOPIXEL_COUNT 1 // How many neopixels are connected +#define NEOPIXEL_DATA 21 // gpio pin used to send data to the neopixels +#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use + +// The usbPower state is revered ? +// DEBUG | ??:??:?? 365 [Power] Battery: usbPower=0, isCharging=0, batMv=4116, batPct=90 +// DEBUG | ??:??:?? 385 [Power] Battery: usbPower=1, isCharging=1, batMv=4243, batPct=0 + +// https://www.waveshare.com/img/devkit/ESP32-S3-Pico/ESP32-S3-Pico-details-inter-1.jpg +// digram is incorrect labeled as battery pin is getting readings on GPIO7_CH1? +#define BATTERY_PIN 7 +#define ADC_CHANNEL ADC1_GPIO7_CHANNEL +// #define ADC_CHANNEL ADC1_GPIO6_CHANNEL +// ratio of voltage divider = 3.0 (R17=200k, R18=100k) +#define ADC_MULTIPLIER 3.1 // 3.0 + a bit for being optimistic + +#define I2C_SDA 15 +#define I2C_SCL 16 + +// Enable secondary bus for external periherals +// https://www.waveshare.com/wiki/Pico-OLED-1.3 +// #define USE_SH1107_128_64 +// Not working +#define I2C_SDA1 17 +#define I2C_SCL1 18 + +#define BUTTON_PIN 0 // This is the BOOT button +#define BUTTON_NEED_PULLUP + +// #define USE_RF95 // RFM95/SX127x +#define USE_SX1262 +// #define USE_SX1280 + +#define LORA_MISO 37 +#define LORA_SCK 35 +#define LORA_MOSI 36 +#define LORA_CS 14 + +#define LORA_RESET 40 +#define LORA_DIO1 4 +#define LORA_DIO2 13 + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif + +#ifdef USE_SX1280 +#define SX128X_CS LORA_CS +#define SX128X_DIO1 LORA_DIO1 +#define SX128X_BUSY 9 +#define SX128X_RESET LORA_RESET +#endif + +#define USE_EINK +/* + * eink display pins + */ +#define PIN_EINK_CS 34 +#define PIN_EINK_BUSY 38 +#define PIN_EINK_DC 33 +#define PIN_EINK_RES 42 // 37 //(-1) // cant be MISO Waveshare ??) +#define PIN_EINK_SCLK 35 +#define PIN_EINK_MOSI 36 \ No newline at end of file diff --git a/variants/feather_diy/platformio.ini b/variants/feather_diy/platformio.ini new file mode 100644 index 0000000..82dbb31 --- /dev/null +++ b/variants/feather_diy/platformio.ini @@ -0,0 +1,12 @@ +; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921 +[env:feather_diy] +extends = nrf52840_base +board = adafruit_feather_nrf52840 +build_flags = ${nrf52840_base.build_flags} -Ivariants/feather_diy -Dfeather_diy + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/feather_diy> +lib_deps = + ${nrf52840_base.lib_deps} +debug_tool = jlink +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +;upload_protocol = jlink \ No newline at end of file diff --git a/variants/feather_diy/variant.cpp b/variants/feather_diy/variant.cpp new file mode 100644 index 0000000..7311c90 --- /dev/null +++ b/variants/feather_diy/variant.cpp @@ -0,0 +1,24 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" diff --git a/variants/feather_diy/variant.h b/variants/feather_diy/variant.h new file mode 100644 index 0000000..1c0979f --- /dev/null +++ b/variants/feather_diy/variant.h @@ -0,0 +1,119 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_FEATHER_DIY_ +#define _VARIANT_FEATHER_DIY_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (0 + 12) // P0.12 22 +#define PIN_WIRE_SCL (0 + 11) // P0.12 23 + +#define PIN_LED1 (32 + 15) // P1.15 3 +#define PIN_LED2 (32 + 10) // P1.10 4 + +#define LED_BUILTIN PIN_LED1 + +#define LED_GREEN PIN_LED2 // Actually red +#define LED_BLUE PIN_LED1 + +#define LED_STATE_ON 1 // State when LED is litted + +#define BUTTON_PIN (32 + 2) // P1.02 7 + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (0 + 24) // P0.24 1 +#define PIN_SERIAL1_TX (0 + 25) // P0.25 0 + +#define PIN_SERIAL2_RX (-1) +#define PIN_SERIAL2_TX (-1) + +#define SPI_INTERFACES_COUNT 1 + +#define PIN_SPI_MISO (0 + 15) // P0.15 24 +#define PIN_SPI_MOSI (0 + 13) // P0.13 25 +#define PIN_SPI_SCK (0 + 14) // P0.14 26 + +#define SS 2 + +#define LORA_DIO0 -1 // a No connect on the SX1262/SX1268 module +#define LORA_RESET (32 + 9) // P1.09 13 // RST for SX1276, and for SX1262/SX1268 +#define LORA_DIO1 (0 + 6) // P0.06 11 // IRQ for SX1262/SX1268 +#define LORA_DIO2 (0 + 8) // P0.08 12 // BUSY for SX1262/SX1268 +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262/SX1268, if DIO3 is high the TXCO is enabled + +#define LORA_SCK SCK +#define LORA_MISO MI +#define LORA_MOSI MO +#define LORA_CS SS + +// enables 3.3V periphery like GPS or IO Module +#define PIN_3V3_EN (-1) + +#undef USE_EINK + +// supported modules list +#define USE_SX1262 + +// common pinouts for SX126X modules +#define SX126X_CS LORA_CS // NSS for SX126X +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_RXEN (0 + 27) // P0.27 10 +#define SX126X_TXEN (0 + 26) // P0.26 9 + +#ifdef EBYTE_E22 +// Internally the TTGO module hooks the SX126x-DIO2 in to control the TX/RX switch +// (which is the default for the sx1262interface code) +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif diff --git a/variants/feather_rp2040_rfm95/platformio.ini b/variants/feather_rp2040_rfm95/platformio.ini new file mode 100644 index 0000000..a28ad76 --- /dev/null +++ b/variants/feather_rp2040_rfm95/platformio.ini @@ -0,0 +1,16 @@ +[env:feather_rp2040_rfm95] +extends = rp2040_base +board = adafruit_feather +upload_protocol = picotool + +# add our variants files to the include and src paths +build_flags = ${rp2040_base.build_flags} + -DRP2040_FEATHER_RFM95 + -Ivariants/feather_rp2040_rfm95 + -DDEBUG_RP2040_PORT=Serial + -DHW_SPI1_DEVICE + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m0plus" +lib_deps = + ${rp2040_base.lib_deps} +debug_build_flags = ${rp2040_base.build_flags} +debug_tool = cmsis-dap ; for e.g. Picotool \ No newline at end of file diff --git a/variants/feather_rp2040_rfm95/variant.h b/variants/feather_rp2040_rfm95/variant.h new file mode 100644 index 0000000..e9e1782 --- /dev/null +++ b/variants/feather_rp2040_rfm95/variant.h @@ -0,0 +1,61 @@ +// #define RADIOLIB_CUSTOM_ARDUINO 1 +// #define RADIOLIB_TONE_UNSUPPORTED 1 +// #define RADIOLIB_SOFTWARE_SERIAL_UNSUPPORTED 1 + +#define ARDUINO_ARCH_AVR + +// #define USE_SSD1306 + +// #define USE_SH1106 1 + +// default I2C pins: +// SDA = 4 +// SCL = 5 + +// Recommended pins for SerialModule: +// txd = 8 +// rxd = 9 + +#define EXT_NOTIFY_OUT 22 +#define BUTTON_PIN 7 +// #define BUTTON_NEED_PULLUP + +#define LED_PIN PIN_LED + +// #define BATTERY_PIN 26 +// ratio of voltage divider = 3.0 (R17=200k, R18=100k) +// #define ADC_MULTIPLIER 3.1 // 3.0 + a bit for being optimistic + +#define USE_RF95 // RFM95/SX127x + +#undef LORA_SCK +#undef LORA_MISO +#undef LORA_MOSI +#undef LORA_CS + +// https://www.adafruit.com/product/5714 +// https://learn.adafruit.com/feather-rp2040-rfm95 +// https://learn.adafruit.com/assets/120283 +// https://learn.adafruit.com/assets/120813 +#define LORA_SCK 14 // 10 12P +#define LORA_MISO 8 // 12 10P +#define LORA_MOSI 15 // 11 11P +#define LORA_CS 16 // 3 13P + +#define LORA_RESET 17 // 15 14P + +#define LORA_DIO0 21 // ?? 6P +#define LORA_DIO1 22 // 20 7P +#define LORA_DIO2 23 // 2 8P +#define LORA_DIO3 19 // ?? 3P +#define LORA_DIO4 20 // ?? 4P +#define LORA_DIO5 18 // ?? 15P + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +// #define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif \ No newline at end of file diff --git a/variants/heltec_capsule_sensor_v3/platformio.ini b/variants/heltec_capsule_sensor_v3/platformio.ini new file mode 100644 index 0000000..f1aef92 --- /dev/null +++ b/variants/heltec_capsule_sensor_v3/platformio.ini @@ -0,0 +1,11 @@ +[env:heltec_capsule_sensor_v3] +extends = esp32s3_base +board = heltec_wifi_lora_32_V3 +board_check = true + +build_flags = + ${esp32s3_base.build_flags} -I variants/heltec_capsule_sensor_v3 + -D HELTEC_CAPSULE_SENSOR_V3 + -D GPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + ;-D DEBUG_DISABLED ; uncomment this line to disable DEBUG output + diff --git a/variants/heltec_capsule_sensor_v3/variant.h b/variants/heltec_capsule_sensor_v3/variant.h new file mode 100644 index 0000000..415de05 --- /dev/null +++ b/variants/heltec_capsule_sensor_v3/variant.h @@ -0,0 +1,53 @@ +#define LED_PIN 33 +#define LED_PIN2 34 +#define EXT_PWR_DETECT 35 + +#define BUTTON_PIN 18 + +#define BATTERY_PIN 7 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +#define ADC_CHANNEL ADC1_GPIO7_CHANNEL +#define ADC_ATTENUATION ADC_ATTEN_DB_2_5 // lower dB for high resistance voltage divider +#define ADC_MULTIPLIER (4.9 * 1.045) +#define ADC_CTRL 36 // active HIGH, powers the voltage divider. Only on 1.1 +#define ADC_CTRL_ENABLED HIGH + +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define GPS_RX_PIN 5 +#define GPS_TX_PIN 4 +#define PIN_GPS_RESET 3 +#define GPS_RESET_MODE LOW +#define PIN_GPS_PPS 1 +#define PIN_GPS_EN 21 +#define GPS_EN_ACTIVE HIGH + +#define USE_SX1262 +#define LORA_DIO0 -1 // a No connect on the SX1262 module +#define LORA_RESET 12 +#define LORA_DIO1 14 // SX1262 IRQ +#define LORA_DIO2 13 // SX1262 BUSY +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled + +#define LORA_SCK 9 +#define LORA_MISO 11 +#define LORA_MOSI 10 +#define LORA_CS 8 + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET + +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#define I2C_SDA 1 +#define I2C_SCL 2 +#define HAS_SCREEN 0 +#define SENSOR_POWER_CTRL_PIN 21 +#define SENSOR_POWER_ON 1 + +#define PERIPHERAL_WARMUP_MS 100 +#define SENSOR_GPS_CONFLICT + +#define ESP32S3_WAKE_TYPE ESP_EXT1_WAKEUP_ANY_HIGH \ No newline at end of file diff --git a/variants/heltec_esp32c3/pins_arduino.h b/variants/heltec_esp32c3/pins_arduino.h new file mode 100644 index 0000000..a717a37 --- /dev/null +++ b/variants/heltec_esp32c3/pins_arduino.h @@ -0,0 +1,24 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +static const uint8_t TX = 21; +static const uint8_t RX = 20; + +static const uint8_t SDA = 1; +static const uint8_t SCL = 0; + +static const uint8_t SS = 8; +static const uint8_t MOSI = 7; +static const uint8_t MISO = 6; +static const uint8_t SCK = 10; + +static const uint8_t A0 = 0; +static const uint8_t A1 = 1; +static const uint8_t A2 = 2; +static const uint8_t A3 = 3; +static const uint8_t A4 = 4; +static const uint8_t A5 = 5; + +#endif /* Pins_Arduino_h */ \ No newline at end of file diff --git a/variants/heltec_esp32c3/platformio.ini b/variants/heltec_esp32c3/platformio.ini new file mode 100644 index 0000000..6fe5c3c --- /dev/null +++ b/variants/heltec_esp32c3/platformio.ini @@ -0,0 +1,11 @@ +[env:heltec-ht62-esp32c3-sx1262] +extends = esp32c3_base +board = esp32-c3-devkitm-1 +build_flags = + ${esp32_base.build_flags} + -D HELTEC_HT62 + -I variants/heltec_esp32c3 +monitor_speed = 115200 +upload_protocol = esptool +;upload_port = /dev/ttyUSB0 +upload_speed = 921600 \ No newline at end of file diff --git a/variants/heltec_esp32c3/variant.h b/variants/heltec_esp32c3/variant.h new file mode 100644 index 0000000..ca00c43 --- /dev/null +++ b/variants/heltec_esp32c3/variant.h @@ -0,0 +1,29 @@ +#define BUTTON_PIN 9 + +// LED pin on HT-DEV-ESP_V2 and HT-DEV-ESP_V3 +// https://resource.heltec.cn/download/HT-CT62/HT-CT62_Reference_Design.pdf +// https://resource.heltec.cn/download/HT-DEV-ESP/HT-DEV-ESP_V3_Sch.pdf +#define LED_PIN 2 // LED +#define LED_STATE_ON 1 // State when LED is lit + +#define HAS_SCREEN 0 +#define HAS_GPS 0 +#undef GPS_RX_PIN +#undef GPS_TX_PIN + +#define USE_SX1262 +#define LORA_SCK 10 +#define LORA_MISO 6 +#define LORA_MOSI 7 +#define LORA_CS 8 +#define LORA_DIO0 RADIOLIB_NC +#define LORA_RESET 5 +#define LORA_DIO1 3 +#define LORA_DIO2 RADIOLIB_NC +#define LORA_BUSY 4 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_BUSY +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 \ No newline at end of file diff --git a/variants/heltec_hru_3601/pins_arduino.h b/variants/heltec_hru_3601/pins_arduino.h new file mode 100644 index 0000000..625c57c --- /dev/null +++ b/variants/heltec_hru_3601/pins_arduino.h @@ -0,0 +1,25 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include +#include + +static const uint8_t TX = UART_TX; +static const uint8_t RX = UART_RX; + +static const uint8_t SDA = I2C_SDA; +static const uint8_t SCL = I2C_SCL; + +static const uint8_t SS = 8; +static const uint8_t MOSI = 7; +static const uint8_t MISO = 6; +static const uint8_t SCK = 10; + +static const uint8_t A0 = 0; +static const uint8_t A1 = 1; +static const uint8_t A2 = 2; +static const uint8_t A3 = 3; +static const uint8_t A4 = 4; +static const uint8_t A5 = 5; + +#endif /* Pins_Arduino_h */ diff --git a/variants/heltec_hru_3601/platformio.ini b/variants/heltec_hru_3601/platformio.ini new file mode 100644 index 0000000..3668e72 --- /dev/null +++ b/variants/heltec_hru_3601/platformio.ini @@ -0,0 +1,9 @@ +[env:heltec-hru-3601] +extends = esp32c3_base +board = adafruit_qtpy_esp32c3 +build_flags = + ${esp32_base.build_flags} + -D HELTEC_HRU_3601 + -I variants/heltec_hru_3601 +lib_deps = ${esp32c3_base.lib_deps} + adafruit/Adafruit NeoPixel @ ^1.12.0 diff --git a/variants/heltec_hru_3601/variant.h b/variants/heltec_hru_3601/variant.h new file mode 100644 index 0000000..31783ec --- /dev/null +++ b/variants/heltec_hru_3601/variant.h @@ -0,0 +1,40 @@ +#define BUTTON_PIN 9 + +#define HAS_SCREEN 0 +#define HAS_GPS 0 +#undef GPS_RX_PIN +#undef GPS_TX_PIN + +#define USE_SX1262 +#define LORA_SCK 10 +#define LORA_MISO 6 +#define LORA_MOSI 7 +#define LORA_CS 8 +#define LORA_DIO0 RADIOLIB_NC +#define LORA_RESET 5 +#define LORA_DIO1 3 +#define LORA_DIO2 RADIOLIB_NC +#define LORA_BUSY 4 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_BUSY +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// Vext_Ctrl pin controls 3.3V LDO (U2) which provides power to I2C peripheral +#define PIN_POWER_EN 1 + +// Board has I2C connected to UART0 pins, and no other hardware serial port +#define UART_TX -1 +#define UART_RX -1 + +// Board has I2C connected to U0RXD and U0TXD +#define I2C_SDA 21 +#define I2C_SCL 20 + +// Board has RGB LED on GPIO2 +#define HAS_NEOPIXEL // Enable the use of neopixels +#define NEOPIXEL_COUNT 1 // How many neopixels are connected +#define NEOPIXEL_DATA 2 // gpio pin used to send data to the neopixels +#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use diff --git a/variants/heltec_mesh_node_t114/platformio.ini b/variants/heltec_mesh_node_t114/platformio.ini new file mode 100644 index 0000000..1b06c7f --- /dev/null +++ b/variants/heltec_mesh_node_t114/platformio.ini @@ -0,0 +1,17 @@ +; First prototype nrf52840/sx1262 device +[env:heltec-mesh-node-t114] +extends = nrf52840_base +board = heltec_mesh_node_t114 +debug_tool = jlink + +# add -DCFG_SYSVIEW if you want to use the Segger systemview tool for OS profiling. +build_flags = ${nrf52840_base.build_flags} -Ivariants/heltec_mesh_node_t114 + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" + -DGPS_POWER_TOGGLE + -DHELTEC_T114 + +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/heltec_mesh_node_t114> +lib_deps = + ${nrf52840_base.lib_deps} + lewisxhe/PCF8563_Library@^1.0.1 + https://github.com/meshtastic/st7789#bd33ea58ddfe4a5e4a66d53300ccbd38d66ac21f \ No newline at end of file diff --git a/variants/heltec_mesh_node_t114/variant.cpp b/variants/heltec_mesh_node_t114/variant.cpp new file mode 100644 index 0000000..85c9f4a --- /dev/null +++ b/variants/heltec_mesh_node_t114/variant.cpp @@ -0,0 +1,38 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 - pins 0 and 1 are hardwired for xtal and should never be enabled + 0xff, 0xff, 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, + + // 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); +} diff --git a/variants/heltec_mesh_node_t114/variant.h b/variants/heltec_mesh_node_t114/variant.h new file mode 100644 index 0000000..426085a --- /dev/null +++ b/variants/heltec_mesh_node_t114/variant.h @@ -0,0 +1,218 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_HELTEC_NRF_ +#define _VARIANT_HELTEC_NRF_ +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +#define HELTEC_MESH_NODE_T114 + +#define USE_ST7789 + +#define ST7789_NSS 11 +#define ST7789_RS 12 // DC +#define ST7789_SDA 41 // MOSI +#define ST7789_SCK 40 +#define ST7789_RESET 2 +#define ST7789_MISO -1 +#define ST7789_BUSY -1 +#define VTFT_CTRL 3 +#define VTFT_LEDA 15 +// #define ST7789_BL (32+6) +#define TFT_BACKLIGHT_ON LOW +#define ST7789_SPI_HOST SPI1_HOST +// #define TFT_BL (32+6) +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 16000000 +#define TFT_HEIGHT 135 +#define TFT_WIDTH 240 +#define TFT_OFFSET_X 0 +#define TFT_OFFSET_Y 0 +// #define TFT_OFFSET_ROTATION 0 +// #define SCREEN_ROTATE +// #define SCREEN_TRANSITION_FRAMERATE 5 + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (32 + 3) // green (confirmed on 1.0 board) +#define LED_BLUE PIN_LED1 // fake for bluefruit library +#define LED_GREEN PIN_LED1 +#define LED_BUILTIN LED_GREEN +#define LED_STATE_ON 0 // State when LED is lit + +#define HAS_NEOPIXEL // Enable the use of neopixels +#define NEOPIXEL_COUNT 2 // How many neopixels are connected +#define NEOPIXEL_DATA 14 // gpio pin used to send data to the neopixels +#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use + +/* + * 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 + +/* +No longer populated on PCB +*/ +#define PIN_SERIAL2_RX (0 + 9) +#define PIN_SERIAL2_TX (0 + 10) +// #define PIN_SERIAL2_EN (0 + 17) + +/* + * I2C + */ + +#define WIRE_INTERFACES_COUNT 2 + +// I2C bus 0 +// Routed to footprint for PCF8563TS RTC +// Not populated on T114 V1, maybe in future? +#define PIN_WIRE_SDA (0 + 26) // P0.26 +#define PIN_WIRE_SCL (0 + 27) // P0.27 + +// I2C bus 1 +// Available on header pins, for general use +#define PIN_WIRE1_SDA (0 + 16) // P0.16 +#define PIN_WIRE1_SCL (0 + 13) // P0.13 + +// QSPI Pins +#define PIN_QSPI_SCK (32 + 14) +#define PIN_QSPI_CS (32 + 15) +#define PIN_QSPI_IO0 (32 + 12) // MOSI if using two bit interface +#define PIN_QSPI_IO1 (32 + 13) // MISO if using two bit interface +#define PIN_QSPI_IO2 (0 + 7) // WP if using two bit interface (i.e. not used) +#define PIN_QSPI_IO3 (0 + 5) // HOLD if using two bit interface (i.e. not used) + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES MX25R1635F +#define EXTERNAL_FLASH_USE_QSPI + +/* + * Lora radio + */ + +#define USE_SX1262 +// #define USE_SX1268 +#define SX126X_CS (0 + 24) // FIXME - we really should define LORA_CS instead +#define LORA_CS (0 + 24) +#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 +// CPU? +#define SX126X_BUSY (0 + 17) +#define SX126X_RESET (0 + 25) +// 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 + +#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 + +/* + * GPS pins + */ + +#define GPS_L76K + +// #define PIN_GPS_RESET (32 + 6) // An output to reset L76K GPS. As per datasheet, low for > 100ms will reset the L76K +#define GPS_RESET_MODE LOW +// #define PIN_GPS_EN (21) +#define VEXT_ENABLE (0 + 21) +#define PERIPHERAL_WARMUP_MS 1000 // Make sure I2C QuickLink has stable power before continuing +#define VEXT_ON_VALUE HIGH +// #define GPS_EN_ACTIVE HIGH +#define PIN_GPS_STANDBY (32 + 2) // An output to wake GPS, low means allow sleep, high means force wake +#define PIN_GPS_PPS (32 + 4) +// Seems to be missing on this new board +// #define PIN_GPS_PPS (32 + 4) // Pulse per second input from the GPS +#define GPS_TX_PIN (32 + 5) // This is for bits going TOWARDS the CPU +#define GPS_RX_PIN (32 + 7) // This is for bits going TOWARDS the GPS + +#define GPS_THREAD_INTERVAL 50 + +#define PIN_SERIAL1_RX GPS_TX_PIN +#define PIN_SERIAL1_TX GPS_RX_PIN + +// PCF8563 RTC Module +#define PCF8563_RTC 0x51 + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +// For LORA, spi 0 +#define PIN_SPI_MISO (0 + 23) +#define PIN_SPI_MOSI (0 + 22) +#define PIN_SPI_SCK (0 + 19) + +// #define PIN_PWR_EN (0 + 6) + +// To debug via the segger JLINK console rather than the CDC-ACM serial device +// #define USE_SEGGER + +// Battery +// The battery sense is hooked to pin A0 (4) +// it is defined in the anlaolgue pin section of this file +// and has 12 bit resolution + +#define ADC_CTRL 6 +#define ADC_CTRL_ENABLED HIGH +#define BATTERY_PIN 4 +#define ADC_RESOLUTION 14 + +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER (4.90F) + +#define HAS_RTC 0 +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif diff --git a/variants/heltec_v1/platformio.ini b/variants/heltec_v1/platformio.ini new file mode 100644 index 0000000..ee10ef0 --- /dev/null +++ b/variants/heltec_v1/platformio.ini @@ -0,0 +1,7 @@ +[env:heltec-v1] +;build_type = debug ; to make it possible to step through our jtag debugger +extends = esp32_base +board_level = extra +board = heltec_wifi_lora_32 +build_flags = + ${esp32_base.build_flags} -D HELTEC_V1 -I variants/heltec_v1 \ No newline at end of file diff --git a/variants/heltec_v1/variant.h b/variants/heltec_v1/variant.h new file mode 100644 index 0000000..d1338a2 --- /dev/null +++ b/variants/heltec_v1/variant.h @@ -0,0 +1,31 @@ +// the default ESP32 Pin of 15 is the Oled SCL, set to 36 and 37 and works fine. +// Tested on Neo6m module. +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define GPS_RX_PIN 36 +#define GPS_TX_PIN 33 + +#ifndef USE_JTAG // gpio15 is TDO for JTAG, so no I2C on this board while doing jtag +#define I2C_SDA 4 // I2C pins for this board +#define I2C_SCL 15 +#endif + +#define RESET_OLED 16 // If defined, this pin will be used to reset the display controller + +#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 + +#define USE_RF95 +#define LORA_DIO0 26 // a No connect on the SX1262 module +#ifndef USE_JTAG +#define LORA_RESET 14 +#endif +#define LORA_DIO1 RADIOLIB_NC +#define LORA_DIO2 32 // Not really used + +// ratio of voltage divider = 3.20 (R1=100k, R2=220k) +#define ADC_MULTIPLIER 3.2 + +#define BATTERY_PIN 13 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +#define ADC_CHANNEL ADC2_GPIO13_CHANNEL +#define BAT_MEASURE_ADC_UNIT 2 \ No newline at end of file diff --git a/variants/heltec_v2.1/platformio.ini b/variants/heltec_v2.1/platformio.ini new file mode 100644 index 0000000..ea22819 --- /dev/null +++ b/variants/heltec_v2.1/platformio.ini @@ -0,0 +1,8 @@ +[env:heltec-v2_1] +board_level = extra +;build_type = debug ; to make it possible to step through our jtag debugger +extends = esp32_base +board = heltec_wifi_lora_32_V2 +build_flags = + ${esp32_base.build_flags} -D HELTEC_V2_1 -I variants/heltec_v2.1 + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. \ No newline at end of file diff --git a/variants/heltec_v2.1/variant.h b/variants/heltec_v2.1/variant.h new file mode 100644 index 0000000..8ebccc5 --- /dev/null +++ b/variants/heltec_v2.1/variant.h @@ -0,0 +1,37 @@ +// Pin planning should refer to this document +// https://resource.heltec.cn/download/WiFi_LoRa_32/WIFI_LoRa_32_V2.pdf + +// the default ESP32 Pin of 15 is the Oled SCL, 37 is battery pin. +// Tested on Neo6m module. +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define GPS_RX_PIN 36 +#define GPS_TX_PIN 33 + +#define PIN_GPS_EN 37 // GPS power enable pin + +#ifndef USE_JTAG // gpio15 is TDO for JTAG, so no I2C on this board while doing jtag +#define I2C_SDA 4 // I2C pins for this board +#define I2C_SCL 15 +#endif + +#define RESET_OLED 16 // If defined, this pin will be used to reset the display controller + +#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 + +#define USE_RF95 +#define LORA_DIO0 26 // a No connect on the SX1262 module +#ifndef USE_JTAG +#define LORA_RESET 14 +#endif +#define LORA_DIO1 35 // https://www.thethingsnetwork.org/forum/t/big-esp32-sx127x-topic-part-3/18436 +#define LORA_DIO2 34 // Not really used + +#define ADC_MULTIPLIER 3.2 // 220k + 100k (320k/100k=3.2) +// #define ADC_WIDTH ADC_WIDTH_BIT_10 + +#define BATTERY_PIN 37 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +#define ADC_CHANNEL ADC1_GPIO37_CHANNEL +#define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module. \ No newline at end of file diff --git a/variants/heltec_v2/platformio.ini b/variants/heltec_v2/platformio.ini new file mode 100644 index 0000000..c81bca8 --- /dev/null +++ b/variants/heltec_v2/platformio.ini @@ -0,0 +1,7 @@ +[env:heltec-v2_0] +;build_type = debug ; to make it possible to step through our jtag debugger +board_level = extra +extends = esp32_base +board = heltec_wifi_lora_32_V2 +build_flags = + ${esp32_base.build_flags} -D HELTEC_V2_0 -I variants/heltec_v2 \ No newline at end of file diff --git a/variants/heltec_v2/variant.h b/variants/heltec_v2/variant.h new file mode 100644 index 0000000..5c18381 --- /dev/null +++ b/variants/heltec_v2/variant.h @@ -0,0 +1,31 @@ +// the default ESP32 Pin of 15 is the Oled SCL, set to 36 and 37 and works fine. +// Tested on Neo6m module. +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define GPS_RX_PIN 36 +#define GPS_TX_PIN 33 + +#ifndef USE_JTAG // gpio15 is TDO for JTAG, so no I2C on this board while doing jtag +#define I2C_SDA 4 // I2C pins for this board +#define I2C_SCL 15 +#endif + +#define RESET_OLED 16 // If defined, this pin will be used to reset the display controller + +#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 + +#define USE_RF95 +#define LORA_DIO0 26 // a No connect on the SX1262 module +#ifndef USE_JTAG +#define LORA_RESET 14 +#endif +#define LORA_DIO1 35 // https://www.thethingsnetwork.org/forum/t/big-esp32-sx127x-topic-part-3/18436 +#define LORA_DIO2 34 // Not really used + +// ratio of voltage divider = 3.20 (R12=100k, R10=220k) +#define ADC_MULTIPLIER 3.2 +#define BATTERY_PIN 13 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +#define ADC_CHANNEL ADC2_GPIO13_CHANNEL +#define BAT_MEASURE_ADC_UNIT 2 \ No newline at end of file diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini new file mode 100644 index 0000000..e8f73e1 --- /dev/null +++ b/variants/heltec_v3/platformio.ini @@ -0,0 +1,8 @@ +[env:heltec-v3] +extends = esp32s3_base +board = heltec_wifi_lora_32_V3 +board_check = true +# Temporary until espressif creates a release with this new target +build_flags = + ${esp32s3_base.build_flags} -D HELTEC_V3 -I variants/heltec_v3 + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. \ No newline at end of file diff --git a/variants/heltec_v3/variant.h b/variants/heltec_v3/variant.h new file mode 100644 index 0000000..4f1d91d --- /dev/null +++ b/variants/heltec_v3/variant.h @@ -0,0 +1,42 @@ +#define LED_PIN LED + +#define USE_SSD1306 // Heltec_v3 has a SSD1306 display + +#define RESET_OLED RST_OLED +#define I2C_SDA SDA_OLED // I2C pins for this board +#define I2C_SCL SCL_OLED + +// Enable secondary bus for external periherals +#define I2C_SDA1 SDA +#define I2C_SCL1 SCL + +#define VEXT_ENABLE Vext // active low, powers the oled display and the lora antenna boost +#define BUTTON_PIN 0 + +#define ADC_CTRL 37 +#define ADC_CTRL_ENABLED LOW +#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_ATTENUATION ADC_ATTEN_DB_2_5 // lower dB for high resistance voltage divider +#define ADC_MULTIPLIER 4.9 * 1.045 + +#define USE_SX1262 + +#define LORA_DIO0 -1 // a No connect on the SX1262 module +#define LORA_RESET 12 +#define LORA_DIO1 14 // SX1262 IRQ +#define LORA_DIO2 13 // SX1262 BUSY +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled + +#define LORA_SCK 9 +#define LORA_MISO 11 +#define LORA_MOSI 10 +#define LORA_CS 8 + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET + +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 diff --git a/variants/heltec_vision_master_e213/pins_arduino.h b/variants/heltec_vision_master_e213/pins_arduino.h new file mode 100644 index 0000000..56f5ef1 --- /dev/null +++ b/variants/heltec_vision_master_e213/pins_arduino.h @@ -0,0 +1,61 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +static const uint8_t LED_BUILTIN = 45; // LED is not populated on earliest board variant +#define BUILTIN_LED LED_BUILTIN // Backward compatibility +#define LED_BUILTIN LED_BUILTIN + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 39; +static const uint8_t SCL = 38; + +static const uint8_t SS = 8; +static const uint8_t MOSI = 10; +static const uint8_t MISO = 11; +static const uint8_t SCK = 9; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +static const uint8_t RST_LoRa = 12; +static const uint8_t BUSY_LoRa = 13; +static const uint8_t DIO1 = 14; + +#endif /* Pins_Arduino_h */ diff --git a/variants/heltec_vision_master_e213/platformio.ini b/variants/heltec_vision_master_e213/platformio.ini new file mode 100644 index 0000000..709ae32 --- /dev/null +++ b/variants/heltec_vision_master_e213/platformio.ini @@ -0,0 +1,23 @@ +[env:heltec-vision-master-e213] +extends = esp32s3_base +board = heltec_vision_master_e213 +build_flags = + ${esp32s3_base.build_flags} + -Ivariants/heltec_vision_master_e213 + -DHELTEC_VISION_MASTER_E213 + -DEINK_DISPLAY_MODEL=GxEPD2_213_FC1 + -DEINK_WIDTH=250 + -DEINK_HEIGHT=122 + -DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk + -DEINK_LIMIT_FASTREFRESH=10 ; How many consecutive fast-refreshes are permitted + -DEINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates + -DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates +; -D EINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated + -DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. + -DEINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting" + -DEINK_HASQUIRK_WEAKFASTREFRESH ; Pixels set with fast-refresh are easy to clear, disrupted by sunlight +lib_deps = + ${esp32s3_base.lib_deps} + https://github.com/meshtastic/GxEPD2#b202ebfec6a4821e098cf7a625ba0f6f2400292d + lewisxhe/PCF8563_Library@^1.0.1 +upload_speed = 115200 \ No newline at end of file diff --git a/variants/heltec_vision_master_e213/variant.h b/variants/heltec_vision_master_e213/variant.h new file mode 100644 index 0000000..386df6f --- /dev/null +++ b/variants/heltec_vision_master_e213/variant.h @@ -0,0 +1,56 @@ +#define LED_PIN 45 // LED is not populated on earliest board variant +#define BUTTON_PIN 0 +#define BUTTON_PIN_SECONDARY 21 // Second built-in button +#define BUTTON_SECONDARY_CANNEDMESSAGES // By default, use the secondary button as canned message input + +// I2C +#define I2C_SDA SDA +#define I2C_SCL SCL + +// Display (E-Ink) +#define USE_EINK +#define PIN_EINK_CS 5 +#define PIN_EINK_BUSY 1 +#define PIN_EINK_DC 2 +#define PIN_EINK_RES 3 +#define PIN_EINK_SCLK 4 +#define PIN_EINK_MOSI 6 + +// SPI +#define SPI_INTERFACES_COUNT 2 +#define PIN_SPI_MISO 11 +#define PIN_SPI_MOSI 10 +#define PIN_SPI_SCK 9 + +// Power +#define VEXT_ENABLE 18 // Powers the E-Ink display, and the 3.3V supply to the I2C QuickLink connector +#define PERIPHERAL_WARMUP_MS 1000 // Make sure I2C QuickLink has stable power before continuing +#define VEXT_ON_VALUE HIGH +#define ADC_CTRL 46 +#define ADC_CTRL_ENABLED HIGH +#define BATTERY_PIN 7 +#define ADC_CHANNEL ADC1_GPIO7_CHANNEL +#define ADC_MULTIPLIER 4.9 * 1.03 +#define ADC_ATTENUATION ADC_ATTEN_DB_2_5 +#define HAS_32768HZ + +// LoRa +#define USE_SX1262 + +#define LORA_DIO0 RADIOLIB_NC // a No connect on the SX1262 module +#define LORA_RESET 12 +#define LORA_DIO1 14 // SX1262 IRQ +#define LORA_DIO2 13 // SX1262 BUSY + +#define LORA_SCK 9 +#define LORA_MISO 11 +#define LORA_MOSI 10 +#define LORA_CS 8 + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET + +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 \ No newline at end of file diff --git a/variants/heltec_vision_master_e290/pins_arduino.h b/variants/heltec_vision_master_e290/pins_arduino.h new file mode 100644 index 0000000..56f5ef1 --- /dev/null +++ b/variants/heltec_vision_master_e290/pins_arduino.h @@ -0,0 +1,61 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +static const uint8_t LED_BUILTIN = 45; // LED is not populated on earliest board variant +#define BUILTIN_LED LED_BUILTIN // Backward compatibility +#define LED_BUILTIN LED_BUILTIN + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 39; +static const uint8_t SCL = 38; + +static const uint8_t SS = 8; +static const uint8_t MOSI = 10; +static const uint8_t MISO = 11; +static const uint8_t SCK = 9; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +static const uint8_t RST_LoRa = 12; +static const uint8_t BUSY_LoRa = 13; +static const uint8_t DIO1 = 14; + +#endif /* Pins_Arduino_h */ diff --git a/variants/heltec_vision_master_e290/platformio.ini b/variants/heltec_vision_master_e290/platformio.ini new file mode 100644 index 0000000..e1ba100 --- /dev/null +++ b/variants/heltec_vision_master_e290/platformio.ini @@ -0,0 +1,25 @@ +[env:heltec-vision-master-e290] +extends = esp32s3_base +board = heltec_vision_master_e290 +build_flags = + ${esp32s3_base.build_flags} + -I variants/heltec_vision_master_e290 + -D HELTEC_VISION_MASTER_E290 + -D BUTTON_CLICK_MS=200 + -D EINK_DISPLAY_MODEL=GxEPD2_290_BN8 + -D EINK_WIDTH=296 + -D EINK_HEIGHT=128 + -D USE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk + -D EINK_LIMIT_FASTREFRESH=10 ; How many consecutive fast-refreshes are permitted + -D EINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates + -D EINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates + -D EINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting" + -D EINK_HASQUIRK_WEAKFASTREFRESH ; Pixels set with fast-refresh are easy to clear, disrupted by sunlight +; -D EINK_LIMIT_GHOSTING_PX=2000 ; How much image ghosting is tolerated +; -D EINK_BACKGROUND_USES_FAST ; (If enabled) don't redraw RESPONSIVE frames at next BACKGROUND update + +lib_deps = + ${esp32s3_base.lib_deps} + https://github.com/meshtastic/GxEPD2#448c8538129fde3d02a7cb5e6fc81971ad92547f + lewisxhe/PCF8563_Library@^1.0.1 +upload_speed = 115200 \ No newline at end of file diff --git a/variants/heltec_vision_master_e290/variant.h b/variants/heltec_vision_master_e290/variant.h new file mode 100644 index 0000000..2991865 --- /dev/null +++ b/variants/heltec_vision_master_e290/variant.h @@ -0,0 +1,55 @@ +#define LED_PIN 45 // LED is not populated on earliest board variant +#define BUTTON_PIN 0 +#define BUTTON_PIN_SECONDARY 21 // Second built-in button +#define BUTTON_SECONDARY_CANNEDMESSAGES // By default, use the secondary button as canned message input + +// I2C +#define I2C_SDA SDA +#define I2C_SCL SCL + +// Display (E-Ink) +#define USE_EINK +#define PIN_EINK_CS 3 +#define PIN_EINK_BUSY 6 +#define PIN_EINK_DC 4 +#define PIN_EINK_RES 5 +#define PIN_EINK_SCLK 2 +#define PIN_EINK_MOSI 1 + +// SPI +#define SPI_INTERFACES_COUNT 2 +#define PIN_SPI_MISO 11 +#define PIN_SPI_MOSI 10 +#define PIN_SPI_SCK 9 + +// Power +#define VEXT_ENABLE 18 // Powers the E-Ink display only +#define VEXT_ON_VALUE HIGH +#define ADC_CTRL 46 +#define ADC_CTRL_ENABLED HIGH +#define BATTERY_PIN 7 +#define ADC_CHANNEL ADC1_GPIO7_CHANNEL +#define ADC_MULTIPLIER 4.9 * 1.03 +#define ADC_ATTENUATION ADC_ATTEN_DB_2_5 +#define HAS_32768HZ + +// LoRa +#define USE_SX1262 + +#define LORA_DIO0 RADIOLIB_NC // a No connect on the SX1262 module +#define LORA_RESET 12 +#define LORA_DIO1 14 // SX1262 IRQ +#define LORA_DIO2 13 // SX1262 BUSY + +#define LORA_SCK 9 +#define LORA_MISO 11 +#define LORA_MOSI 10 +#define LORA_CS 8 + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET + +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 \ No newline at end of file diff --git a/variants/heltec_vision_master_t190/pins_arduino.h b/variants/heltec_vision_master_t190/pins_arduino.h new file mode 100644 index 0000000..eeef95f --- /dev/null +++ b/variants/heltec_vision_master_t190/pins_arduino.h @@ -0,0 +1,61 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +static const uint8_t LED_BUILTIN = 35; +#define BUILTIN_LED LED_BUILTIN // backward compatibility +#define LED_BUILTIN LED_BUILTIN + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 2; +static const uint8_t SCL = 1; + +static const uint8_t SS = 8; +static const uint8_t MOSI = 10; +static const uint8_t MISO = 11; +static const uint8_t SCK = 9; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +static const uint8_t RST_LoRa = 12; +static const uint8_t BUSY_LoRa = 13; +static const uint8_t DIO0 = 14; + +#endif /* Pins_Arduino_h */ diff --git a/variants/heltec_vision_master_t190/platformio.ini b/variants/heltec_vision_master_t190/platformio.ini new file mode 100644 index 0000000..0c504d6 --- /dev/null +++ b/variants/heltec_vision_master_t190/platformio.ini @@ -0,0 +1,13 @@ +[env:heltec-vision-master-t190] +extends = esp32s3_base +board = heltec_vision_master_t190 +build_flags = + ${esp32s3_base.build_flags} + -Ivariants/heltec_vision_master_t190 + -DHELTEC_VISION_MASTER_T190 + ; -D PRIVATE_HW +lib_deps = + ${esp32s3_base.lib_deps} + lewisxhe/PCF8563_Library@^1.0.1 + https://github.com/meshtastic/st7789#bd33ea58ddfe4a5e4a66d53300ccbd38d66ac21f +upload_speed = 921600 \ No newline at end of file diff --git a/variants/heltec_vision_master_t190/variant.h b/variants/heltec_vision_master_t190/variant.h new file mode 100644 index 0000000..1da3f99 --- /dev/null +++ b/variants/heltec_vision_master_t190/variant.h @@ -0,0 +1,72 @@ +#define BUTTON_PIN 0 +#define BUTTON_PIN_SECONDARY 21 // Second built-in button +#define BUTTON_SECONDARY_CANNEDMESSAGES // By default, use the secondary button as canned message input + +// I2C +#define I2C_SDA SDA +#define I2C_SCL SCL + +// Display (TFT) +#define USE_ST7789 +#define ST7789_NSS 39 +#define ST7789_RS 47 // DC +#define ST7789_SDA 48 // MOSI +#define ST7789_SCK 38 +#define ST7789_RESET 40 +#define ST7789_MISO 4 +#define ST7789_BUSY -1 +#define VTFT_CTRL 7 +#define VTFT_LEDA 17 +#define TFT_BACKLIGHT_ON HIGH +#define ST7789_SPI_HOST SPI2_HOST +#define SPI_FREQUENCY 10000000 +#define SPI_READ_FREQUENCY 10000000 +#define TFT_HEIGHT 170 +#define TFT_WIDTH 320 +#define TFT_OFFSET_X 0 +#define TFT_OFFSET_Y 0 +// #define TFT_OFFSET_ROTATION 0 +// #define SCREEN_ROTATE +// #define SCREEN_TRANSITION_FRAMERATE 5 +#define BRIGHTNESS_DEFAULT 100 // Medium Low Brightnes + +// #define SLEEP_TIME 120 + +// SPI +#define SPI_INTERFACES_COUNT 2 +#define PIN_SPI_MISO 11 +#define PIN_SPI_MOSI 10 +#define PIN_SPI_SCK 9 + +// Power +#define VEXT_ENABLE 5 +#define VEXT_ON_VALUE HIGH +#define ADC_CTRL 46 +#define ADC_CTRL_ENABLED HIGH +#define BATTERY_PIN 6 +#define ADC_CHANNEL ADC1_GPIO6_CHANNEL +#define ADC_MULTIPLIER 4.9 * 1.03 // Voltage divider is roughly 1:1 +#define ADC_ATTENUATION ADC_ATTEN_DB_2_5 // Voltage divider output is quite high +#define HAS_32768HZ + +// LoRa +#define USE_SX1262 + +#define LORA_DIO0 RADIOLIB_NC // a No connect on the SX1262 module +#define LORA_RESET 12 +#define LORA_DIO1 14 // SX1262 IRQ +#define LORA_DIO2 13 // SX1262 BUSY +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled + +#define LORA_SCK 9 +#define LORA_MISO 11 +#define LORA_MOSI 10 +#define LORA_CS 8 + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET + +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 \ No newline at end of file diff --git a/variants/heltec_wireless_bridge/platformio.ini b/variants/heltec_wireless_bridge/platformio.ini new file mode 100644 index 0000000..45c3aba --- /dev/null +++ b/variants/heltec_wireless_bridge/platformio.ini @@ -0,0 +1,6 @@ +[env:heltec-wireless-bridge] +;build_type = debug ; to make it possible to step through our jtag debugger +extends = esp32_base +board = heltec_wifi_lora_32 +build_flags = + ${esp32_base.build_flags} -D HELTEC_WIRELESS_BRIDGE -I variants/heltec_wireless_bridge \ No newline at end of file diff --git a/variants/heltec_wireless_bridge/variant.h b/variants/heltec_wireless_bridge/variant.h new file mode 100644 index 0000000..7c4f416 --- /dev/null +++ b/variants/heltec_wireless_bridge/variant.h @@ -0,0 +1,29 @@ +// the default ESP32 Pin of 15 is the Oled SCL, set to 36 and 37 and works fine. +// Tested on Neo6m module. +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define GPS_RX_PIN 36 +#define GPS_TX_PIN 33 + +#ifndef USE_JTAG // gpio15 is TDO for JTAG, so no I2C on this board while doing jtag +#define I2C_SDA 4 // I2C pins for this board +#define I2C_SCL 15 +#endif + +#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 + +#define USE_RF95 +#define LORA_DIO0 26 // a No connect on the SX1262 module +#ifndef USE_JTAG +#define LORA_RESET 14 +#endif +#define LORA_DIO1 35 +#define LORA_DIO2 34 // Not really used + +// ratio of voltage divider = 3.20 (R1=100k, R2=220k) +#define ADC_MULTIPLIER 3.2 + +#define BATTERY_PIN 13 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +#define ADC_CHANNEL ADC2_GPIO13_CHANNEL +#define BAT_MEASURE_ADC_UNIT 2 \ No newline at end of file diff --git a/variants/heltec_wireless_paper/pins_arduino.h b/variants/heltec_wireless_paper/pins_arduino.h new file mode 100644 index 0000000..3e36d98 --- /dev/null +++ b/variants/heltec_wireless_paper/pins_arduino.h @@ -0,0 +1,61 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +static const uint8_t LED_BUILTIN = 18; +#define BUILTIN_LED LED_BUILTIN // backward compatibility +#define LED_BUILTIN LED_BUILTIN + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 41; +static const uint8_t SCL = 42; + +static const uint8_t SS = 8; +static const uint8_t MOSI = 10; +static const uint8_t MISO = 11; +static const uint8_t SCK = 9; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +static const uint8_t RST_LoRa = 12; +static const uint8_t BUSY_LoRa = 13; +static const uint8_t DIO1 = 14; + +#endif /* Pins_Arduino_h */ diff --git a/variants/heltec_wireless_paper/platformio.ini b/variants/heltec_wireless_paper/platformio.ini new file mode 100644 index 0000000..afbbd8b --- /dev/null +++ b/variants/heltec_wireless_paper/platformio.ini @@ -0,0 +1,23 @@ +[env:heltec-wireless-paper] +extends = esp32s3_base +board = heltec_wifi_lora_32_V3 +build_flags = + ${esp32s3_base.build_flags} + -I variants/heltec_wireless_paper + -D HELTEC_WIRELESS_PAPER + -D EINK_DISPLAY_MODEL=GxEPD2_213_FC1 + -D EINK_WIDTH=250 + -D EINK_HEIGHT=122 + -D USE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk + -D EINK_LIMIT_FASTREFRESH=10 ; How many consecutive fast-refreshes are permitted + -D EINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates + -D EINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates +; -D EINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated + -D EINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. + -D EINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting" + -D EINK_HASQUIRK_WEAKFASTREFRESH ; Pixels set with fast-refresh are easy to clear, disrupted by sunlight +lib_deps = + ${esp32s3_base.lib_deps} + https://github.com/meshtastic/GxEPD2#b202ebfec6a4821e098cf7a625ba0f6f2400292d + lewisxhe/PCF8563_Library@^1.0.1 +upload_speed = 115200 \ No newline at end of file diff --git a/variants/heltec_wireless_paper/variant.h b/variants/heltec_wireless_paper/variant.h new file mode 100644 index 0000000..520dcec --- /dev/null +++ b/variants/heltec_wireless_paper/variant.h @@ -0,0 +1,54 @@ +#define LED_PIN 18 +#define BUTTON_PIN 0 + +// I2C +#define I2C_SDA SDA +#define I2C_SCL SCL + +// Display (E-Ink) +#define USE_EINK +#define PIN_EINK_CS 4 +#define PIN_EINK_BUSY 7 +#define PIN_EINK_DC 5 +#define PIN_EINK_RES 6 +#define PIN_EINK_SCLK 3 +#define PIN_EINK_MOSI 2 + +// SPI +#define SPI_INTERFACES_COUNT 2 +#define PIN_SPI_MISO 11 +#define PIN_SPI_MOSI 10 +#define PIN_SPI_SCK 9 + +// Power +#define VEXT_ENABLE 45 // Active low, powers the E-Ink display +#define VEXT_ON_VALUE LOW +#define ADC_CTRL 19 +#define BATTERY_PIN 20 +#define ADC_CHANNEL ADC2_GPIO20_CHANNEL +#define ADC_MULTIPLIER 2 // Voltage divider is roughly 1:1 +#define BAT_MEASURE_ADC_UNIT 2 // Use ADC2 +#define ADC_ATTENUATION ADC_ATTEN_DB_12 // Voltage divider output is quite high +#define HAS_32768HZ +#define ADC_CTRL_ENABLED LOW + +// LoRa +#define USE_SX1262 + +#define LORA_DIO0 RADIOLIB_NC // a No connect on the SX1262 module +#define LORA_RESET 12 +#define LORA_DIO1 14 // SX1262 IRQ +#define LORA_DIO2 13 // SX1262 BUSY + +#define LORA_SCK 9 +#define LORA_MISO 11 +#define LORA_MOSI 10 +#define LORA_CS 8 + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET + +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 \ No newline at end of file diff --git a/variants/heltec_wireless_paper_v1/pins_arduino.h b/variants/heltec_wireless_paper_v1/pins_arduino.h new file mode 100644 index 0000000..2bb4416 --- /dev/null +++ b/variants/heltec_wireless_paper_v1/pins_arduino.h @@ -0,0 +1,66 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +static const uint8_t LED_BUILTIN = 18; +#define BUILTIN_LED LED_BUILTIN // backward compatibility +#define LED_BUILTIN LED_BUILTIN + +static const uint8_t KEY_BUILTIN = 0; + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 41; +static const uint8_t SCL = 42; + +static const uint8_t SS = 8; +static const uint8_t MOSI = 10; +static const uint8_t MISO = 11; +static const uint8_t SCK = 9; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +static const uint8_t Vext = 45; +static const uint8_t LED = 18; + +static const uint8_t RST_LoRa = 12; +static const uint8_t BUSY_LoRa = 13; +static const uint8_t DIO1 = 14; + +#endif /* Pins_Arduino_h */ diff --git a/variants/heltec_wireless_paper_v1/platformio.ini b/variants/heltec_wireless_paper_v1/platformio.ini new file mode 100644 index 0000000..999f158 --- /dev/null +++ b/variants/heltec_wireless_paper_v1/platformio.ini @@ -0,0 +1,22 @@ +[env:heltec-wireless-paper-v1_0] +extends = esp32s3_base +board = heltec_wifi_lora_32_V3 +build_flags = + ${esp32s3_base.build_flags} + -I variants/heltec_wireless_paper_v1 + -D HELTEC_WIRELESS_PAPER_V1_0 + -D EINK_DISPLAY_MODEL=GxEPD2_213_BN + -D EINK_WIDTH=250 + -D EINK_HEIGHT=122 + -D USE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk + -D EINK_LIMIT_FASTREFRESH=5 ; How many consecutive fast-refreshes are permitted + -D EINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates + -D EINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates + -D EINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated + ;-D EINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. + -D EINK_HASQUIRK_VICIOUSFASTREFRESH ; Identify that pixels drawn by fast-refresh are harder to clear +lib_deps = + ${esp32s3_base.lib_deps} + https://github.com/meshtastic/GxEPD2#55f618961db45a23eff0233546430f1e5a80f63a + lewisxhe/PCF8563_Library@^1.0.1 +upload_speed = 115200 \ No newline at end of file diff --git a/variants/heltec_wireless_paper_v1/variant.h b/variants/heltec_wireless_paper_v1/variant.h new file mode 100644 index 0000000..520dcec --- /dev/null +++ b/variants/heltec_wireless_paper_v1/variant.h @@ -0,0 +1,54 @@ +#define LED_PIN 18 +#define BUTTON_PIN 0 + +// I2C +#define I2C_SDA SDA +#define I2C_SCL SCL + +// Display (E-Ink) +#define USE_EINK +#define PIN_EINK_CS 4 +#define PIN_EINK_BUSY 7 +#define PIN_EINK_DC 5 +#define PIN_EINK_RES 6 +#define PIN_EINK_SCLK 3 +#define PIN_EINK_MOSI 2 + +// SPI +#define SPI_INTERFACES_COUNT 2 +#define PIN_SPI_MISO 11 +#define PIN_SPI_MOSI 10 +#define PIN_SPI_SCK 9 + +// Power +#define VEXT_ENABLE 45 // Active low, powers the E-Ink display +#define VEXT_ON_VALUE LOW +#define ADC_CTRL 19 +#define BATTERY_PIN 20 +#define ADC_CHANNEL ADC2_GPIO20_CHANNEL +#define ADC_MULTIPLIER 2 // Voltage divider is roughly 1:1 +#define BAT_MEASURE_ADC_UNIT 2 // Use ADC2 +#define ADC_ATTENUATION ADC_ATTEN_DB_12 // Voltage divider output is quite high +#define HAS_32768HZ +#define ADC_CTRL_ENABLED LOW + +// LoRa +#define USE_SX1262 + +#define LORA_DIO0 RADIOLIB_NC // a No connect on the SX1262 module +#define LORA_RESET 12 +#define LORA_DIO1 14 // SX1262 IRQ +#define LORA_DIO2 13 // SX1262 BUSY + +#define LORA_SCK 9 +#define LORA_MISO 11 +#define LORA_MOSI 10 +#define LORA_CS 8 + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET + +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 \ No newline at end of file diff --git a/variants/heltec_wireless_tracker/pins_arduino.h b/variants/heltec_wireless_tracker/pins_arduino.h new file mode 100644 index 0000000..1052af9 --- /dev/null +++ b/variants/heltec_wireless_tracker/pins_arduino.h @@ -0,0 +1,72 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include "soc/soc_caps.h" +#include + +#define WIFI_LoRa_32_V3 true +#define DISPLAY_HEIGHT 80 +#define DISPLAY_WIDTH 160 + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +static const uint8_t LED_BUILTIN = 18; +#define BUILTIN_LED LED_BUILTIN // backward compatibility +#define LED_BUILTIN LED_BUILTIN + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 45; +static const uint8_t SCL = 46; + +static const uint8_t SS = 8; +static const uint8_t MOSI = 10; +static const uint8_t MISO = 11; +static const uint8_t SCK = 9; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +static const uint8_t Vext = 36; +static const uint8_t LED = 18; + +static const uint8_t RST_LoRa = 12; +static const uint8_t BUSY_LoRa = 13; +static const uint8_t DIO0 = 14; + +#endif /* Pins_Arduino_h */ diff --git a/variants/heltec_wireless_tracker/platformio.ini b/variants/heltec_wireless_tracker/platformio.ini new file mode 100644 index 0000000..c7ecce8 --- /dev/null +++ b/variants/heltec_wireless_tracker/platformio.ini @@ -0,0 +1,14 @@ +[env:heltec-wireless-tracker] +extends = esp32s3_base +board = heltec_wireless_tracker +upload_protocol = esptool + +build_flags = + ${esp32s3_base.build_flags} -I variants/heltec_wireless_tracker + -D HELTEC_TRACKER_V1_1 + -D GPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + ;-D DEBUG_DISABLED ; uncomment this line to disable DEBUG output + +lib_deps = + ${esp32s3_base.lib_deps} + lovyan03/LovyanGFX@^1.1.8 diff --git a/variants/heltec_wireless_tracker/variant.h b/variants/heltec_wireless_tracker/variant.h new file mode 100644 index 0000000..79fa0e8 --- /dev/null +++ b/variants/heltec_wireless_tracker/variant.h @@ -0,0 +1,78 @@ +#define LED_PIN 18 + +#define _VARIANT_HELTEC_WIRELESS_TRACKER +#define HELTEC_TRACKER_V1_X + +// I2C +#define I2C_SDA SDA +#define I2C_SCL SCL + +// ST7735S TFT LCD +#define ST7735S 1 // there are different (sub-)versions of ST7735 +#define ST7735_CS 38 +#define ST7735_RS 40 // DC +#define ST7735_SDA 42 // MOSI +#define ST7735_SCK 41 +#define ST7735_RESET 39 +#define ST7735_MISO -1 +#define ST7735_BUSY -1 +#define TFT_BL 21 /* V1.1 PCB marking */ +#define ST7735_SPI_HOST SPI3_HOST +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 16000000 +#define SCREEN_ROTATE +#define TFT_HEIGHT DISPLAY_WIDTH +#define TFT_WIDTH DISPLAY_HEIGHT +#define TFT_OFFSET_X 26 +#define TFT_OFFSET_Y -1 +#define SCREEN_TRANSITION_FRAMERATE 3 // fps +#define DISPLAY_FORCE_SMALL_FONTS + +// 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) + +#define VEXT_ENABLE 3 // active HIGH - powers the GPS, GPS LNA and OLED +#define VEXT_ON_VALUE HIGH +#define BUTTON_PIN 0 + +#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_ATTENUATION ADC_ATTEN_DB_2_5 // lower dB for high resistance voltage divider +#define ADC_MULTIPLIER 4.9 * 1.045 +#define ADC_CTRL 2 // active HIGH, powers the voltage divider. Only on 1.1 +#define ADC_USE_PULLUP // Use internal pullup/pulldown instead of actively driving the output + +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define GPS_RX_PIN 33 +#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 GPS_RESET_MODE LOW +#define GPS_UC6580 +#define GPS_BAUDRATE 115200 + +#define USE_SX1262 +#define LORA_DIO0 -1 // a No connect on the SX1262 module +#define LORA_RESET 12 +#define LORA_DIO1 14 // SX1262 IRQ +#define LORA_DIO2 13 // SX1262 BUSY +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled + +#define LORA_SCK 9 +#define LORA_MISO 11 +#define LORA_MOSI 10 +#define LORA_CS 8 + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET + +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 \ No newline at end of file diff --git a/variants/heltec_wireless_tracker_V1_0/pins_arduino.h b/variants/heltec_wireless_tracker_V1_0/pins_arduino.h new file mode 100644 index 0000000..28b9820 --- /dev/null +++ b/variants/heltec_wireless_tracker_V1_0/pins_arduino.h @@ -0,0 +1,72 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include "soc/soc_caps.h" +#include + +#define WIFI_LoRa_32_V3 true +#define DISPLAY_HEIGHT 80 +#define DISPLAY_WIDTH 160 + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +static const uint8_t LED_BUILTIN = 18; +#define BUILTIN_LED LED_BUILTIN // backward compatibility +#define LED_BUILTIN LED_BUILTIN + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 5; +static const uint8_t SCL = 6; + +static const uint8_t SS = 8; +static const uint8_t MOSI = 10; +static const uint8_t MISO = 11; +static const uint8_t SCK = 9; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +static const uint8_t Vext = 36; +static const uint8_t LED = 18; + +static const uint8_t RST_LoRa = 12; +static const uint8_t BUSY_LoRa = 13; +static const uint8_t DIO0 = 14; + +#endif /* Pins_Arduino_h */ diff --git a/variants/heltec_wireless_tracker_V1_0/platformio.ini b/variants/heltec_wireless_tracker_V1_0/platformio.ini new file mode 100644 index 0000000..303e27d --- /dev/null +++ b/variants/heltec_wireless_tracker_V1_0/platformio.ini @@ -0,0 +1,14 @@ +[env:heltec-wireless-tracker-V1-0] +extends = esp32s3_base +board = heltec_wireless_tracker +upload_protocol = esptool + +build_flags = + ${esp32s3_base.build_flags} -I variants/heltec_wireless_tracker_V1_0 + -D HELTEC_TRACKER_V1_0 + -D GPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + ;-D DEBUG_DISABLED ; uncomment this line to disable DEBUG output + +lib_deps = + ${esp32s3_base.lib_deps} + lovyan03/LovyanGFX@^1.1.8 \ No newline at end of file diff --git a/variants/heltec_wireless_tracker_V1_0/variant.h b/variants/heltec_wireless_tracker_V1_0/variant.h new file mode 100644 index 0000000..876ff11 --- /dev/null +++ b/variants/heltec_wireless_tracker_V1_0/variant.h @@ -0,0 +1,72 @@ +#define LED_PIN 18 + +#define HELTEC_TRACKER_V1_X + +// I2C +#define I2C_SDA SDA +#define I2C_SCL SCL + +// ST7735S TFT LCD +#define ST7735S 1 // there are different (sub-)versions of ST7735 +#define ST7735_CS 38 +#define ST7735_RS 40 // DC +#define ST7735_SDA 42 // MOSI +#define ST7735_SCK 41 +#define ST7735_RESET 39 +#define ST7735_MISO -1 +#define ST7735_BUSY -1 +#define TFT_BL 45 +#define ST7735_SPI_HOST SPI3_HOST +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 16000000 +#define SCREEN_ROTATE +#define TFT_HEIGHT DISPLAY_WIDTH +#define TFT_WIDTH DISPLAY_HEIGHT +#define TFT_OFFSET_X 26 +#define TFT_OFFSET_Y -1 +#define VTFT_CTRL 46 // Heltec Tracker needs this pulled low for TFT +#define SCREEN_TRANSITION_FRAMERATE 3 // fps +#define DISPLAY_FORCE_SMALL_FONTS + +#define VEXT_ENABLE Vext // active low, powers the oled display and the lora antenna boost +#define VEXT_ON_VALUE LOW +#define BUTTON_PIN 0 + +#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_ATTENUATION ADC_ATTEN_DB_2_5 // lower dB for high resistance voltage divider +#define ADC_MULTIPLIER 4.9 * 1.045 + +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define GPS_RX_PIN 33 +#define GPS_TX_PIN 34 +#define PIN_GPS_RESET 35 +#define PIN_GPS_PPS 36 + +#define PIN_GPS_EN 37 // Heltec Tracker needs this pulled low for GPS +#define GPS_EN_ACTIVE LOW + +#define GPS_RESET_MODE LOW +#define GPS_UC6580 +#define GPS_BAUDRATE 115200 + +#define USE_SX1262 +#define LORA_DIO0 -1 // a No connect on the SX1262 module +#define LORA_RESET 12 +#define LORA_DIO1 14 // SX1262 IRQ +#define LORA_DIO2 13 // SX1262 BUSY +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled + +#define LORA_SCK 9 +#define LORA_MISO 11 +#define LORA_MOSI 10 +#define LORA_CS 8 + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET + +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 \ No newline at end of file diff --git a/variants/heltec_wsl_v3/platformio.ini b/variants/heltec_wsl_v3/platformio.ini new file mode 100644 index 0000000..c956591 --- /dev/null +++ b/variants/heltec_wsl_v3/platformio.ini @@ -0,0 +1,7 @@ +[env:heltec-wsl-v3] +extends = esp32s3_base +board = heltec_wifi_lora_32_V3 +# Temporary until espressif creates a release with this new target +build_flags = + ${esp32s3_base.build_flags} -D HELTEC_WSL_V3 -I variants/heltec_wsl_v3 + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. \ No newline at end of file diff --git a/variants/heltec_wsl_v3/variant.h b/variants/heltec_wsl_v3/variant.h new file mode 100644 index 0000000..c103b91 --- /dev/null +++ b/variants/heltec_wsl_v3/variant.h @@ -0,0 +1,36 @@ +#define I2C_SCL SCL +#define I2C_SDA SDA + +#define LED_PIN LED + +#define VEXT_ENABLE Vext // active low, powers the oled display and the lora antenna boost +#define VEXT_ON_VALUE LOW +#define BUTTON_PIN 0 + +#define ADC_CTRL 37 +#define ADC_CTRL_ENABLED LOW +#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_ATTENUATION ADC_ATTEN_DB_2_5 // lower dB for high resistance voltage divider +#define ADC_MULTIPLIER 4.9 * 1.045 + +#define USE_SX1262 + +#define LORA_DIO0 -1 // a No connect on the SX1262 module +#define LORA_RESET 12 +#define LORA_DIO1 14 // SX1262 IRQ +#define LORA_DIO2 13 // SX1262 BUSY +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled + +#define LORA_SCK 9 +#define LORA_MISO 11 +#define LORA_MOSI 10 +#define LORA_CS 8 + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET + +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 \ No newline at end of file diff --git a/variants/icarus/pins_arduino.h b/variants/icarus/pins_arduino.h new file mode 100644 index 0000000..9837a3b --- /dev/null +++ b/variants/icarus/pins_arduino.h @@ -0,0 +1,22 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x2886 +#define USB_PID 0x0059 + +// GPIO48 Reference: https://github.com/espressif/arduino-esp32/pull/8600 + +// The default Wire will be mapped to Screen and Sensors +static const uint8_t SDA = 8; +static const uint8_t SCL = 9; + +// Default SPI will be mapped to Radio +static const uint8_t MISO = 39; +static const uint8_t SCK = 21; +static const uint8_t MOSI = 38; +static const uint8_t SS = 17; + +#endif /* Pins_Arduino_h */ + \ No newline at end of file diff --git a/variants/icarus/platformio.ini b/variants/icarus/platformio.ini new file mode 100644 index 0000000..11f09ca --- /dev/null +++ b/variants/icarus/platformio.ini @@ -0,0 +1,19 @@ +[env:icarus] +extends = esp32s3_base +board = icarus +board_level = extra +board_check = true +board_build.mcu = esp32s3 +upload_protocol = esptool +upload_speed = 921600 +platform_packages = framework-arduinoespressif32@https://github.com/PowerFeather/powerfeather-meshtastic-arduino-lib/releases/download/2.0.16a/esp32-2.0.16.zip +lib_deps = + ${esp32s3_base.lib_deps} +build_unflags = + ${esp32s3_base.build_unflags} + -DARDUINO_USB_MODE=1 +build_flags = + ${esp32s3_base.build_flags} -D PRIVATE_HW -I variants/icarus + -DBOARD_HAS_PSRAM + + -DARDUINO_USB_MODE=0 diff --git a/variants/icarus/variant.h b/variants/icarus/variant.h new file mode 100644 index 0000000..c9c74b4 --- /dev/null +++ b/variants/icarus/variant.h @@ -0,0 +1,31 @@ +// Icarus has a 1.3 inch OLED Screen +#define SCREEN_SSD106 + +#define I2C_SDA 8 +#define I2C_SCL 9 + +#define I2C_SDA1 18 +#define I2C_SCL1 6 + +#define BUTTON_PIN 7 // Selection button + +// RA-01SH/HT-RA62 LORA module +#define USE_SX1262 + +#define LORA_MISO 39 +#define LORA_SCK 21 +#define LORA_MOSI 38 +#define LORA_CS 17 + +#define LORA_RESET 42 +#define LORA_DIO1 5 + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY 47 +#define SX126X_RESET LORA_RESET + +// DIO2 controlls an antenna switch +#define SX126X_DIO2_AS_RF_SWITCH +#endif diff --git a/variants/m5stack-stamp-c3/pins_arduino.h b/variants/m5stack-stamp-c3/pins_arduino.h new file mode 100644 index 0000000..22d2af5 --- /dev/null +++ b/variants/m5stack-stamp-c3/pins_arduino.h @@ -0,0 +1,24 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +static const uint8_t TX = -1; // 21; +static const uint8_t RX = -1; // 20; + +static const uint8_t SDA = 1; +static const uint8_t SCL = 0; + +static const uint8_t SS = 7; +static const uint8_t MOSI = 6; +static const uint8_t MISO = 5; +static const uint8_t SCK = 4; + +static const uint8_t A0 = 0; +static const uint8_t A1 = 1; +static const uint8_t A2 = 2; +static const uint8_t A3 = 3; +static const uint8_t A4 = 4; +static const uint8_t A5 = 5; + +#endif /* Pins_Arduino_h */ diff --git a/variants/m5stack-stamp-c3/platformio.ini b/variants/m5stack-stamp-c3/platformio.ini new file mode 100644 index 0000000..bab65b6 --- /dev/null +++ b/variants/m5stack-stamp-c3/platformio.ini @@ -0,0 +1,12 @@ +[env:m5stack-stamp-c3] +extends = esp32c3_base +board = esp32-c3-devkitm-1 +board_level = extra +build_flags = + ${esp32_base.build_flags} + -D PRIVATE_HW + -I variants/m5stack-stamp-c3 +monitor_speed = 115200 +upload_protocol = esptool +;upload_port = /dev/ttyACM2 +upload_speed = 921600 diff --git a/variants/m5stack-stamp-c3/variant.h b/variants/m5stack-stamp-c3/variant.h new file mode 100644 index 0000000..8242ef4 --- /dev/null +++ b/variants/m5stack-stamp-c3/variant.h @@ -0,0 +1,73 @@ +#define I2C_SDA 1 +#define I2C_SCL 0 + +#define BUTTON_PIN 3 // M5Stack STAMP C3 built in button +#define BUTTON_NEED_PULLUP + +// #define HAS_SCREEN 0 +#define HAS_GPS 0 +#undef GPS_RX_PIN +#undef GPS_TX_PIN + +#undef LORA_SCK +#undef LORA_MISO +#undef LORA_MOSI +#undef LORA_CS + +// Adafruit RFM95W OK +// https://www.adafruit.com/product/3072 +#define USE_RF95 +#define LORA_SCK 4 +#define LORA_MISO 5 +#define LORA_MOSI 6 +#define LORA_CS 7 +#define LORA_DIO0 10 +#define LORA_RESET 8 +#define LORA_DIO1 RADIOLIB_NC +#define LORA_DIO2 RADIOLIB_NC + +// WaveShare Core1262-868M OK +// https://www.waveshare.com/wiki/Core1262-868M +// #define USE_SX1262 +// #define LORA_SCK 4 +// #define LORA_MISO 5 +// #define LORA_MOSI 6 +// #define LORA_CS 7 +// #define LORA_DIO0 RADIOLIB_NC +// #define LORA_RESET 8 +// #define LORA_DIO1 10 +// #define LORA_DIO2 RADIOLIB_NC +// #define LORA_BUSY 18 +// #define SX126X_CS LORA_CS +// #define SX126X_DIO1 LORA_DIO1 +// #define SX126X_BUSY LORA_BUSY +// #define SX126X_RESET LORA_RESET +// #define SX126X_DIO2_AS_RF_SWITCH +// #define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// SX128X 2.4 Ghz LoRa module Not OK - RadioLib issue ? still to confirm +// #define USE_SX1280 +// #define LORA_SCK 4 +// #define LORA_MISO 5 +// #define LORA_MOSI 6 +// #define LORA_CS 7 +// #define LORA_DIO0 -1 +// #define LORA_DIO1 10 +// #define LORA_DIO2 21 +// #define LORA_RESET 8 +// #define LORA_BUSY 1 +// #define SX128X_CS LORA_CS +// #define SX128X_DIO1 LORA_DIO1 +// #define SX128X_BUSY LORA_BUSY +// #define SX128X_RESET LORA_RESET +// #define SX128X_MAX_POWER 10 + +// Not yet tested +// #define USE_EINK +// #define PIN_EINK_EN -1 // N/C +// #define PIN_EINK_CS 9 // EPD_CS +// #define PIN_EINK_BUSY 18 // EPD_BUSY +// #define PIN_EINK_DC 19 // EPD_D/C +// #define PIN_EINK_RES -1 // Connected but not needed +// #define PIN_EINK_SCLK 4 // EPD_SCLK +// #define PIN_EINK_MOSI 6 // EPD_MOSI diff --git a/variants/m5stack_core/pins_arduino.h b/variants/m5stack_core/pins_arduino.h new file mode 100644 index 0000000..cf807aa --- /dev/null +++ b/variants/m5stack_core/pins_arduino.h @@ -0,0 +1,47 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +static const uint8_t TX = 1; +static const uint8_t RX = 3; + +static const uint8_t TXD2 = 17; +static const uint8_t RXD2 = 16; + +static const uint8_t SDA = 21; +static const uint8_t SCL = 22; + +static const uint8_t SS = 5; +static const uint8_t MOSI = 23; +static const uint8_t MISO = 19; +static const uint8_t SCK = 18; + +static const uint8_t G23 = 23; +static const uint8_t G19 = 19; +static const uint8_t G18 = 18; +static const uint8_t G3 = 3; +static const uint8_t G16 = 16; +static const uint8_t G21 = 21; +static const uint8_t G2 = 2; +static const uint8_t G12 = 12; +static const uint8_t G15 = 15; +static const uint8_t G35 = 35; +static const uint8_t G36 = 36; +static const uint8_t G25 = 25; +static const uint8_t G26 = 26; +static const uint8_t G1 = 1; +static const uint8_t G17 = 17; +static const uint8_t G22 = 22; +static const uint8_t G5 = 5; +static const uint8_t G13 = 13; +static const uint8_t G0 = 0; +static const uint8_t G34 = 34; + +static const uint8_t DAC1 = 25; +static const uint8_t DAC2 = 26; + +static const uint8_t ADC1 = 35; +static const uint8_t ADC2 = 36; + +#endif /* Pins_Arduino_h */ diff --git a/variants/m5stack_core/platformio.ini b/variants/m5stack_core/platformio.ini new file mode 100644 index 0000000..95f5aea --- /dev/null +++ b/variants/m5stack_core/platformio.ini @@ -0,0 +1,28 @@ +[env:m5stack-core] +extends = esp32_base +board = m5stack-core-esp32 +monitor_filters = esp32_exception_decoder +build_src_filter = + ${esp32_base.build_src_filter} +build_flags = + ${esp32_base.build_flags} -I variants/m5stack_core + -DILI9341_DRIVER + -DM5STACK + -DUSER_SETUP_LOADED + -DTFT_SDA_READ + -DTFT_DRIVER=0x9341 + -DTFT_MISO=19 + -DTFT_MOSI=23 + -DTFT_SCLK=18 + -DTFT_CS=14 + -DTFT_DC=27 + -DTFT_RST=33 + -DTFT_BL=32 + -DSPI_FREQUENCY=40000000 + -DSPI_READ_FREQUENCY=16000000 + -DDISABLE_ALL_LIBRARY_WARNINGS +lib_ignore = + m5stack-core +lib_deps = + ${esp32_base.lib_deps} + lovyan03/LovyanGFX@^1.1.8 \ No newline at end of file diff --git a/variants/m5stack_core/variant.h b/variants/m5stack_core/variant.h new file mode 100644 index 0000000..72aeb16 --- /dev/null +++ b/variants/m5stack_core/variant.h @@ -0,0 +1,46 @@ +// #define BUTTON_NEED_PULLUP // if set we need to turn on the internal CPU pullup during sleep + +#define I2C_SDA 21 +#define I2C_SCL 22 + +// #define BUTTON_PIN 39 // 38, 37 +// #define BUTTON_PIN 0 +#define BUTTON_NEED_PULLUP +// #define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Plugin. + +#define BUTTON_PIN 38 + +#define PIN_BUZZER 25 + +#undef LORA_SCK +#undef LORA_MISO +#undef LORA_MOSI +#undef LORA_CS + +#define LORA_SCK 18 +#define LORA_MISO 19 +#define LORA_MOSI 23 +#define LORA_CS 5 + +#define USE_RF95 +#define LORA_DIO0 36 // a No connect on the SX1262 module +#define LORA_RESET 26 +#define LORA_DIO1 RADIOLIB_NC // Not really used +#define LORA_DIO2 RADIOLIB_NC // Not really used + +// This board has different GPS pins than all other boards +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define GPS_RX_PIN 16 +#define GPS_TX_PIN 17 + +#define TFT_HEIGHT 240 +#define TFT_WIDTH 320 +#define TFT_OFFSET_X 0 +#define TFT_OFFSET_Y 0 +#define TFT_BUSY -1 + +// LCD screens are slow, so slowdown the wipe so it looks better +#define SCREEN_TRANSITION_FRAMERATE 1 // fps + +#define ILI9341_SPI_HOST VSPI_HOST // VSPI_HOST or HSPI_HOST diff --git a/variants/m5stack_coreink/pins_arduino.h b/variants/m5stack_coreink/pins_arduino.h new file mode 100644 index 0000000..c75283a --- /dev/null +++ b/variants/m5stack_coreink/pins_arduino.h @@ -0,0 +1,49 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define TX2 -1 +#define RX2 -1 + +static const uint8_t TX = 1; +static const uint8_t RX = 3; + +static const uint8_t SDA = 32; +static const uint8_t SCL = 33; + +static const uint8_t SS = 9; +static const uint8_t MOSI = 23; +static const uint8_t MISO = 34; +static const uint8_t SCK = 18; + +static const uint8_t G26 = 26; +static const uint8_t G36 = 36; +static const uint8_t G25 = 25; + +static const uint8_t G32 = 32; +static const uint8_t G33 = 33; + +static const uint8_t G21 = 21; +static const uint8_t G22 = 22; + +static const uint8_t G13 = 13; +static const uint8_t G14 = 14; + +static const uint8_t G12 = 12; +static const uint8_t G19 = 19; + +static const uint8_t G5 = 5; +static const uint8_t G10 = 10; +static const uint8_t G2 = 2; +static const uint8_t G37 = 37; +static const uint8_t G38 = 38; +static const uint8_t G39 = 39; + +static const uint8_t DAC1 = 25; +static const uint8_t DAC2 = 26; + +static const uint8_t ADC1 = 35; +static const uint8_t ADC2 = 36; + +#endif /* Pins_Arduino_h */ diff --git a/variants/m5stack_coreink/platformio.ini b/variants/m5stack_coreink/platformio.ini new file mode 100644 index 0000000..c0c8bd3 --- /dev/null +++ b/variants/m5stack_coreink/platformio.ini @@ -0,0 +1,27 @@ +[env:m5stack-coreink] +extends = esp32_base +board = m5stack-coreink +board_check = true +build_src_filter = + ${esp32_base.build_src_filter} +build_flags = + ${esp32_base.build_flags} -I variants/m5stack_coreink + ;-D RADIOLIB_VERBOSE + -Ofast + -D__MCUXPRESSO + -DEINK_DISPLAY_MODEL=GxEPD2_154_M09 + -DEINK_WIDTH=200 + -DEINK_HEIGHT=200 + -DUSER_SETUP_LOADED + -DM5_COREINK + -DM5STACK +lib_deps = + ${esp32_base.lib_deps} + zinggjm/GxEPD2@^1.5.3 + lewisxhe/PCF8563_Library@^1.0.1 +lib_ignore = + m5stack-coreink +monitor_filters = esp32_exception_decoder +board_build.f_cpu = 240000000L +upload_protocol = esptool +upload_port = /dev/ttyACM0 \ No newline at end of file diff --git a/variants/m5stack_coreink/variant.h b/variants/m5stack_coreink/variant.h new file mode 100644 index 0000000..ecd93b7 --- /dev/null +++ b/variants/m5stack_coreink/variant.h @@ -0,0 +1,109 @@ +// Primary I2C Bus includes PCF8563 RTC Module +#define I2C_SDA 21 +#define I2C_SCL 22 + +#define HAS_GPS 1 +#undef GPS_RX_PIN +#undef GPS_TX_PIN +// Use Secondary I2C Bus as GPS Serial +#define GPS_RX_PIN 33 +// #define GPS_TX_PIN 32 (now used by SX1262 BUSY as GPS works with just RX) + +// Green LED +#define LED_STATE_ON 1 // State when LED is lit +#define LED_PIN 10 + +#include "pcf8563.h" +// PCF8563 RTC Module +#define PCF8563_RTC 0x51 +#define HAS_RTC 1 + +// Wheel +// Down 37 +// Push 38 +// Up 39 +// Top Physical Button 5 + +#define BUTTON_NEED_PULLUP +#define BUTTON_PIN 5 + +// BUZZER +#define PIN_BUZZER 2 + +#undef LORA_SCK +#undef LORA_MISO +#undef LORA_MOSI +#undef LORA_CS + +#define USE_RF95 +// #define USE_SX1262 +// #define USE_SX1280 + +#ifdef USE_RF95 +#define LORA_SCK 18 +#define LORA_MISO 34 +#define LORA_MOSI 23 +#define LORA_CS 14 +#define LORA_DIO0 25 +#define LORA_RESET 26 +#define LORA_DIO1 RADIOLIB_NC +#define LORA_DIO2 RADIOLIB_NC +#endif + +// https://www.waveshare.com/core1262-868m.htm +#ifdef USE_SX1262 +#define LORA_SCK 18 +#define LORA_MISO 34 +#define LORA_MOSI 23 +#define LORA_CS 14 +#define LORA_RESET 26 +#define LORA_DIO1 25 +#define LORA_DIO2 32 // 33 // (13 not working) //BUSY pin on SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif + +#ifdef USE_SX1280 +#define LORA_SCK 18 +#define LORA_MISO 34 +#define LORA_MOSI 23 +#define LORA_CS 14 +#define LORA_RESET 26 +#define LORA_DIO1 25 +#define LORA_DIO2 13 +#define SX128X_CS LORA_CS +#define SX128X_DIO1 LORA_DIO1 +#define SX128X_BUSY LORA_DIO2 +#define SX128X_RESET LORA_RESET +#define SX128X_MAX_POWER 13 // 10 +#endif + +#define USE_EINK +// https://docs.m5stack.com/en/core/coreink +// https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/schematic/Core/coreink/coreink_sch.pdf +#define PIN_EINK_EN -1 // N/C +#define PIN_EINK_CS 9 // EPD_CS +#define PIN_EINK_BUSY 4 // EPD_BUSY +#define PIN_EINK_DC 15 // EPD_D/C +#define PIN_EINK_RES -1 // Connected but not needed +#define PIN_EINK_SCLK 18 // EPD_SCLK +#define PIN_EINK_MOSI 23 // EPD_MOSI + +#define BATTERY_PIN 35 +#define ADC_CHANNEL ADC1_GPIO35_CHANNEL +// https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/schematic/Core/m5paper/M5_PAPER_SCH.pdf +// https://github.com/m5stack/M5Core-Ink/blob/master/examples/Basics/FactoryTest/FactoryTest.ino#L58 +// VBAT +// | +// R83 (3K) +// + +// R86 (11K) +// | +// GND +// https://github.com/m5stack/M5Core-Ink/blob/master/examples/Basics/FactoryTest/FactoryTest.ino#L58 +#define ADC_MULTIPLIER 5 +// https://embeddedexplorer.com/esp32-adc-esp-idf-tutorial/ \ No newline at end of file diff --git a/variants/m5stack_cores3/pins_arduino.h b/variants/m5stack_cores3/pins_arduino.h new file mode 100644 index 0000000..78e9369 --- /dev/null +++ b/variants/m5stack_cores3/pins_arduino.h @@ -0,0 +1,63 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include "soc/soc_caps.h" +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// Some boards have too low voltage on this pin (board design bug) +// Use different pin with 3V and connect with 48 +// and change this setup for the chosen pin (for example 38) +static const uint8_t LED_BUILTIN = SOC_GPIO_PIN_COUNT + 48; +#define BUILTIN_LED LED_BUILTIN // backward compatibility +#define LED_BUILTIN LED_BUILTIN +#define RGB_BUILTIN LED_BUILTIN +#define RGB_BRIGHTNESS 64 + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t TXD2 = 17; +static const uint8_t RXD2 = 18; + +static const uint8_t SDA = 12; +static const uint8_t SCL = 11; + +static const uint8_t SS = 15; +static const uint8_t MOSI = 37; +static const uint8_t MISO = 35; +static const uint8_t SCK = 36; + +static const uint8_t G0 = 0; +static const uint8_t G1 = 1; +static const uint8_t G2 = 2; +static const uint8_t G3 = 3; +static const uint8_t G4 = 4; +static const uint8_t G5 = 5; +static const uint8_t G6 = 6; +static const uint8_t G7 = 7; +static const uint8_t G8 = 8; +static const uint8_t G9 = 9; +static const uint8_t G11 = 11; +static const uint8_t G12 = 12; +static const uint8_t G13 = 13; +static const uint8_t G14 = 14; +static const uint8_t G17 = 17; +static const uint8_t G18 = 18; +static const uint8_t G19 = 19; +static const uint8_t G20 = 20; +static const uint8_t G21 = 21; +static const uint8_t G33 = 33; +static const uint8_t G34 = 34; +static const uint8_t G35 = 35; +static const uint8_t G36 = 36; +static const uint8_t G37 = 37; +static const uint8_t G38 = 38; +static const uint8_t G45 = 45; +static const uint8_t G46 = 46; + +static const uint8_t ADC = 10; + +#endif /* Pins_Arduino_h */ diff --git a/variants/m5stack_cores3/platformio.ini b/variants/m5stack_cores3/platformio.ini new file mode 100644 index 0000000..fc73fab --- /dev/null +++ b/variants/m5stack_cores3/platformio.ini @@ -0,0 +1,14 @@ +; M5stack CoreS3 +[env:m5stack-cores3] +extends = esp32s3_base +board = m5stack-cores3 +board_check = true +upload_protocol = esptool + +build_flags = ${esp32_base.build_flags} + -DPRIVATE_HW + -DM5STACK_CORES3 + -Ivariants/m5stack_cores3 + +lib_deps = + ${esp32_base.lib_deps} diff --git a/variants/m5stack_cores3/variant.h b/variants/m5stack_cores3/variant.h new file mode 100644 index 0000000..2ad4fcb --- /dev/null +++ b/variants/m5stack_cores3/variant.h @@ -0,0 +1,22 @@ +#define I2C_SDA 12 +#define I2C_SCL 11 + +#undef LORA_SCK +#undef LORA_MISO +#undef LORA_MOSI +#undef LORA_CS + +#define LORA_SCK 36 +#define LORA_MISO 35 +#define LORA_MOSI 37 +#define LORA_CS 6 // NSS + +#define USE_RF95 +#define LORA_DIO0 14 // IRQ +#define LORA_RESET 5 // RESET +#define LORA_RST 5 // RESET +#define LORA_IRQ 14 // DIO0 +#define LORA_DIO1 RADIOLIB_NC // Not really used +#define LORA_DIO2 RADIOLIB_NC // Not really used + +#define HAS_AXP2101 diff --git a/variants/monteops_hw1/platformio.ini b/variants/monteops_hw1/platformio.ini new file mode 100644 index 0000000..eaa2465 --- /dev/null +++ b/variants/monteops_hw1/platformio.ini @@ -0,0 +1,15 @@ +; MonteOps M.Node/M.Backbone/M.Eagle hardware based on hardware variant #1 (RAK4630 based) +[env:monteops_hw1] +board_level = extra +extends = nrf52840_base +board = wiscore_rak4631 +build_flags = ${nrf52840_base.build_flags} -Ivariants/monteops_hw1 -D MONTEOPS_HW1 + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/monteops_hw1> + + + +lib_deps = + ${nrf52840_base.lib_deps} + ${networking_base.lib_deps} + https://github.com/RAKWireless/RAK13800-W5100S.git#1.0.2 +debug_tool = jlink +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +;upload_protocol = jlink diff --git a/variants/monteops_hw1/variant.cpp b/variants/monteops_hw1/variant.cpp new file mode 100644 index 0000000..75cca1d --- /dev/null +++ b/variants/monteops_hw1/variant.cpp @@ -0,0 +1,41 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 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, + + // 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); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); +} diff --git a/variants/monteops_hw1/variant.h b/variants/monteops_hw1/variant.h new file mode 100644 index 0000000..97536b1 --- /dev/null +++ b/variants/monteops_hw1/variant.h @@ -0,0 +1,233 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_MOPS_HW1_ +#define _VARIANT_MOPS_HW1_ + +#define RAK4630 + +// MonteOps hardware design variant +#ifndef MONTEOPS_HW1 +#define MONTEOPS_HW1 +#endif + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (35) +#define PIN_LED2 (36) // Connected to WWAN host LED (if present) + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 + +#define LED_STATE_ON 1 // State when LED is litted + +/* + * Buttons + */ + +// #define PIN_BUTTON1 9 // Pin for button on E-ink button module or IO expansion +#define BUTTON_NEED_PULLUP +#define PIN_BUTTON2 12 +#define PIN_BUTTON3 24 +#define PIN_BUTTON4 25 + +/* + * Analog pins + */ +#define PIN_A0 (5) +#define PIN_A1 (31) +#define PIN_A2 (28) +#define PIN_A3 (29) +#define PIN_A4 (30) +#define PIN_A5 (31) +#define PIN_A6 (0xff) +#define PIN_A7 (0xff) + +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; +static const uint8_t A3 = PIN_A3; +static const uint8_t A4 = PIN_A4; +static const uint8_t A5 = PIN_A5; +static const uint8_t A6 = PIN_A6; +static const uint8_t A7 = PIN_A7; +#define ADC_RESOLUTION 14 + +// Other pins +#define PIN_AREF (2) +#define PIN_NFC1 (9) +#define PIN_NFC2 (10) + +static const uint8_t AREF = PIN_AREF; + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (15) +#define PIN_SERIAL1_TX (16) + +// Connected to Jlink CDC +#define PIN_SERIAL2_RX (8) +#define PIN_SERIAL2_TX (6) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +#define PIN_SPI_MISO (45) +#define PIN_SPI_MOSI (44) +#define PIN_SPI_SCK (43) + +#define PIN_SPI1_MISO (29) // (0 + 29) +#define PIN_SPI1_MOSI (30) // (0 + 30) +#define PIN_SPI1_SCK (3) // (0 + 3) + +static const uint8_t SS = 42; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (13) +#define PIN_WIRE_SCL (14) + +// QSPI Pins +#define PIN_QSPI_SCK 3 +#define PIN_QSPI_CS 26 +#define PIN_QSPI_IO0 30 +#define PIN_QSPI_IO1 29 +#define PIN_QSPI_IO2 28 +#define PIN_QSPI_IO3 2 + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES IS25LP080D +#define EXTERNAL_FLASH_USE_QSPI + +/* @note RAK5005-O GPIO mapping to RAK4631 GPIO ports + RAK5005-O <-> nRF52840 + IO1 <-> P0.17 (Arduino GPIO number 17) + IO2 <-> P1.02 (Arduino GPIO number 34) + IO3 <-> P0.21 (Arduino GPIO number 21) + IO4 <-> P0.04 (Arduino GPIO number 4) + IO5 <-> P0.09 (Arduino GPIO number 9) + IO6 <-> P0.10 (Arduino GPIO number 10) + IO7 <-> P0.28 (Arduino GPIO number 28) + SW1 <-> P0.01 (Arduino GPIO number 1) + A0 <-> P0.04/AIN2 (Arduino Analog A2) + A1 <-> P0.31/AIN7 (Arduino Analog A7) + SPI_CS <-> P0.26 (Arduino GPIO number 26) + */ + +// RAK4630 LoRa module + +/* Setup of the SX1262 LoRa module ( https://docs.rakwireless.com/Product-Categories/WisBlock/RAK4631/Datasheet/ ) + +P1.10 NSS SPI NSS (Arduino GPIO number 42) +P1.11 SCK SPI CLK (Arduino GPIO number 43) +P1.12 MOSI SPI MOSI (Arduino GPIO number 44) +P1.13 MISO SPI MISO (Arduino GPIO number 45) +P1.14 BUSY BUSY signal (Arduino GPIO number 46) +P1.15 DIO1 DIO1 event interrupt (Arduino GPIO number 47) +P1.06 NRESET NRESET manual reset of the SX1262 (Arduino GPIO number 38) + +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 + +SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG + +*/ + +#define USE_SX1262 +#define SX126X_CS (42) +#define SX126X_DIO1 (47) +#define SX126X_BUSY (46) +#define SX126X_RESET (38) +// #define SX126X_TXEN (39) +// #define SX126X_RXEN (37) +#define SX126X_POWER_EN (37) + +#define SX126X_DIO2_AS_RF_SWITCH // DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#define PIN_GPS_RESET (34) // Must be P1.02 +// #define PIN_GPS_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 + +// Battery +// The battery sense is hooked to pin A0 (5) +#define BATTERY_PIN PIN_A0 +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER (1.73F) + +// #define HAS_RTC 1 + +#define HAS_ETHERNET 1 + +#define PIN_ETHERNET_RESET 21 +#define PIN_ETHERNET_SS 26 // P0.26 QSPI_CS +#define ETH_SPI_PORT SPI1 +#define AQ_SET_PIN 10 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif \ No newline at end of file diff --git a/variants/my_esp32s3_diy_eink/pins_arduino.h b/variants/my_esp32s3_diy_eink/pins_arduino.h new file mode 100644 index 0000000..b37a258 --- /dev/null +++ b/variants/my_esp32s3_diy_eink/pins_arduino.h @@ -0,0 +1,26 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// The default Wire will be mapped to PMU and RTC +static const uint8_t SDA = 18; +static const uint8_t SCL = 17; + +// Default SPI will be mapped to Radio +static const uint8_t MISO = 3; +static const uint8_t SCK = 5; +static const uint8_t MOSI = 6; +static const uint8_t SS = 7; + +// #define SPI_MOSI (11) +// #define SPI_SCK (14) +// #define SPI_MISO (2) +// #define SPI_CS (13) + +// #define SDCARD_CS SPI_CS + +#endif /* Pins_Arduino_h */ diff --git a/variants/my_esp32s3_diy_eink/platformio.ini b/variants/my_esp32s3_diy_eink/platformio.ini new file mode 100644 index 0000000..e81f2c1 --- /dev/null +++ b/variants/my_esp32s3_diy_eink/platformio.ini @@ -0,0 +1,27 @@ +[env:my-esp32s3-diy-eink] +board_level = extra +extends = esp32s3_base +board = my_esp32s3_diy_eink +board_build.arduino.memory_type = dio_opi +board_build.mcu = esp32s3 +board_build.f_cpu = 240000000L +upload_protocol = esptool +;upload_port = /dev/ttyACM1 +upload_speed = 921600 +platform_packages = + tool-esptoolpy@^1.40500.0 +lib_deps = + ${esp32_base.lib_deps} + zinggjm/GxEPD2@^1.5.1 + adafruit/Adafruit NeoPixel @ ^1.12.0 +build_unflags = -DARDUINO_USB_MODE=1 +build_flags = + ;${esp32_base.build_flags} -D MY_ESP32S3_DIY -I variants/my_esp32s3_diy_eink + ${esp32_base.build_flags} -D PRIVATE_HW -I variants/my_esp32s3_diy_eink + -Dmy + -DEINK_DISPLAY_MODEL=GxEPD2_290_T5D + -DEINK_WIDTH=296 + -DEINK_HEIGHT=128 + -DBOARD_HAS_PSRAM + -mfix-esp32-psram-cache-issue + -DARDUINO_USB_MODE=0 \ No newline at end of file diff --git a/variants/my_esp32s3_diy_eink/variant.h b/variants/my_esp32s3_diy_eink/variant.h new file mode 100644 index 0000000..024f912 --- /dev/null +++ b/variants/my_esp32s3_diy_eink/variant.h @@ -0,0 +1,60 @@ +#define HAS_GPS 0 +#undef GPS_RX_PIN +#undef GPS_TX_PIN + +// #define HAS_SCREEN 0 +// #define HAS_SDCARD +// #define SDCARD_USE_SPI1 + +// #define USE_SSD1306 + +#define I2C_SDA 18 // 1 // I2C pins for this board +#define I2C_SCL 17 // 2 + +// #define LED_PIN 38 // This is a RGB LED not a standard LED +#define HAS_NEOPIXEL // Enable the use of neopixels +#define NEOPIXEL_COUNT 1 // How many neopixels are connected +#define NEOPIXEL_DATA 38 // gpio pin used to send data to the neopixels +#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use + +#define BUTTON_PIN 0 // This is the BOOT button +#define BUTTON_NEED_PULLUP + +// #define USE_RF95 // RFM95/SX127x +// #define USE_SX1262 +#define USE_SX1280 + +#define LORA_MISO 3 +#define LORA_SCK 5 +#define LORA_MOSI 6 +#define LORA_CS 7 + +#define LORA_RESET 8 +#define LORA_DIO1 16 + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS // FIXME - we really should define LORA_CS instead +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY 15 +#define SX126X_RESET LORA_RESET +#define SX126X_RXEN 4 +#define SX126X_TXEN 9 +#endif + +#ifdef USE_SX1280 +#define SX128X_CS LORA_CS +#define SX128X_DIO1 LORA_DIO1 +#define SX128X_BUSY 15 +#define SX128X_RESET LORA_RESET +#endif + +#define USE_EINK +/* + * eink display pins + */ +#define PIN_EINK_CS 13 +#define PIN_EINK_BUSY 2 +#define PIN_EINK_DC 1 +#define PIN_EINK_RES (-1) +#define PIN_EINK_SCLK 5 +#define PIN_EINK_MOSI 6 \ No newline at end of file diff --git a/variants/my_esp32s3_diy_oled/pins_arduino.h b/variants/my_esp32s3_diy_oled/pins_arduino.h new file mode 100644 index 0000000..b37a258 --- /dev/null +++ b/variants/my_esp32s3_diy_oled/pins_arduino.h @@ -0,0 +1,26 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// The default Wire will be mapped to PMU and RTC +static const uint8_t SDA = 18; +static const uint8_t SCL = 17; + +// Default SPI will be mapped to Radio +static const uint8_t MISO = 3; +static const uint8_t SCK = 5; +static const uint8_t MOSI = 6; +static const uint8_t SS = 7; + +// #define SPI_MOSI (11) +// #define SPI_SCK (14) +// #define SPI_MISO (2) +// #define SPI_CS (13) + +// #define SDCARD_CS SPI_CS + +#endif /* Pins_Arduino_h */ diff --git a/variants/my_esp32s3_diy_oled/platformio.ini b/variants/my_esp32s3_diy_oled/platformio.ini new file mode 100644 index 0000000..2d7a5cd --- /dev/null +++ b/variants/my_esp32s3_diy_oled/platformio.ini @@ -0,0 +1,22 @@ +[env:my-esp32s3-diy-oled] +board_level = extra +extends = esp32s3_base +board = my-esp32s3-diy-oled +board_build.arduino.memory_type = dio_opi +board_build.mcu = esp32s3 +board_build.f_cpu = 240000000L +upload_protocol = esptool +;upload_port = /dev/ttyACM0 +upload_speed = 921600 +platform_packages = + tool-esptoolpy@^1.40500.0 +lib_deps = + ${esp32_base.lib_deps} + adafruit/Adafruit NeoPixel @ ^1.12.0 +build_unflags = -DARDUINO_USB_MODE=1 +build_flags = + ;${esp32_base.build_flags} -D MY_ESP32S3_DIY -I variants/my_esp32s3_diy_oled + ${esp32_base.build_flags} -D PRIVATE_HW -I variants/my_esp32s3_diy_oled + -DBOARD_HAS_PSRAM + -mfix-esp32-psram-cache-issue + -DARDUINO_USB_MODE=0 \ No newline at end of file diff --git a/variants/my_esp32s3_diy_oled/variant.h b/variants/my_esp32s3_diy_oled/variant.h new file mode 100644 index 0000000..8a3a390 --- /dev/null +++ b/variants/my_esp32s3_diy_oled/variant.h @@ -0,0 +1,60 @@ +#define HAS_GPS 0 +#undef GPS_RX_PIN +#undef GPS_TX_PIN + +// #define HAS_SCREEN 0 +// #define HAS_SDCARD +// #define SDCARD_USE_SPI1 + +#define USE_SSD1306 + +#define I2C_SDA 18 // 1 // I2C pins for this board +#define I2C_SCL 17 // 2 + +// #define LED_PIN 38 // This is a RGB LED not a standard LED +#define HAS_NEOPIXEL // Enable the use of neopixels +#define NEOPIXEL_COUNT 1 // How many neopixels are connected +#define NEOPIXEL_DATA 38 // gpio pin used to send data to the neopixels +#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use + +#define BUTTON_PIN 0 // This is the BOOT button +#define BUTTON_NEED_PULLUP + +// #define USE_RF95 // RFM95/SX127x +// #define USE_SX1262 +#define USE_SX1280 + +#define LORA_MISO 3 +#define LORA_SCK 5 +#define LORA_MOSI 6 +#define LORA_CS 7 + +#define LORA_RESET 8 +#define LORA_DIO1 16 + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS // FIXME - we really should define LORA_CS instead +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY 15 +#define SX126X_RESET LORA_RESET +#define SX126X_RXEN 4 +#define SX126X_TXEN 9 +#endif + +#ifdef USE_SX1280 +#define SX128X_CS LORA_CS +#define SX128X_DIO1 LORA_DIO1 +#define SX128X_BUSY 15 +#define SX128X_RESET LORA_RESET +#endif + +// #define USE_EINK +/* + * eink display pins + */ +// #define PIN_EINK_CS 13 +// #define PIN_EINK_BUSY 2 +// #define PIN_EINK_DC 1 +// #define PIN_EINK_RES (-1) +// #define PIN_EINK_SCLK 5 +// #define PIN_EINK_MOSI 6 \ No newline at end of file diff --git a/variants/nano-g1-explorer/platformio.ini b/variants/nano-g1-explorer/platformio.ini new file mode 100644 index 0000000..22037cb --- /dev/null +++ b/variants/nano-g1-explorer/platformio.ini @@ -0,0 +1,8 @@ +; The 1.0 release of the nano-g1-explorer board +[env:nano-g1-explorer] +extends = esp32_base +board = ttgo-t-beam +lib_deps = + ${esp32_base.lib_deps} +build_flags = + ${esp32_base.build_flags} -D NANO_G1_EXPLORER -I variants/nano-g1-explorer \ No newline at end of file diff --git a/variants/nano-g1-explorer/variant.h b/variants/nano-g1-explorer/variant.h new file mode 100644 index 0000000..3d5d71a --- /dev/null +++ b/variants/nano-g1-explorer/variant.h @@ -0,0 +1,42 @@ +// #define BUTTON_NEED_PULLUP // if set we need to turn on the internal CPU pullup during sleep + +#define I2C_SDA 21 +#define I2C_SCL 22 + +#define BUTTON_PIN 36 // The user button (information button) GPIO on the Nano G1 explorer +// #define BUTTON_PIN_ALT 13 // Alternate GPIO for an external button if needed. Does anyone use this? It is not documented +// anywhere. +#define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module. + +// 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 +#define USE_SX1262 + +#define GPS_RX_PIN 34 +#define GPS_TX_PIN 12 + +#define LORA_DIO0 26 // a No connect on the SX1262 module +#define LORA_RESET 23 +#define LORA_DIO1 33 // SX1262 IRQ +#define LORA_DIO2 32 // SX1262 BUSY +#define LORA_DIO3 // Not connected on PCB + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS // FIXME - we really should define LORA_CS instead +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +// 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) +#endif + +#define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +#define ADC_CHANNEL ADC1_GPIO35_CHANNEL +#define BATTERY_SENSE_SAMPLES 15 // Set the number of samples, It has an effect of increasing sensitivity. +#define ADC_MULTIPLIER 2 + +#define USE_SH1107_128_64 diff --git a/variants/nano-g1/platformio.ini b/variants/nano-g1/platformio.ini new file mode 100644 index 0000000..a310742 --- /dev/null +++ b/variants/nano-g1/platformio.ini @@ -0,0 +1,8 @@ +; The 1.0 release of the nano-g1 board +[env:nano-g1] +extends = esp32_base +board = ttgo-t-beam +lib_deps = + ${esp32_base.lib_deps} +build_flags = + ${esp32_base.build_flags} -D NANO_G1 -I variants/nano-g1 \ No newline at end of file diff --git a/variants/nano-g1/variant.h b/variants/nano-g1/variant.h new file mode 100644 index 0000000..dd83554 --- /dev/null +++ b/variants/nano-g1/variant.h @@ -0,0 +1,38 @@ +// #define BUTTON_NEED_PULLUP // if set we need to turn on the internal CPU pullup during sleep + +#define I2C_SDA 21 +#define I2C_SCL 22 + +#define BUTTON_PIN 36 // The middle button GPIO on the Nano G1 +// #define BUTTON_PIN_ALT 13 // Alternate GPIO for an external button if needed. Does anyone use this? It is not documented +// anywhere. +#define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module. + +// 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 +#define USE_SX1262 + +#define GPS_RX_PIN 34 +#define GPS_TX_PIN 12 + +#define LORA_DIO0 26 // a No connect on the SX1262 module +#define LORA_RESET 23 +#define LORA_DIO1 33 // SX1262 IRQ +#define LORA_DIO2 32 // SX1262 BUSY +#define LORA_DIO3 // Not connected on PCB + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS // FIXME - we really should define LORA_CS instead +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +// 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) +#endif + +// different screen +#define USE_SH1106 \ No newline at end of file diff --git a/variants/nano-g2-ultra/platformio.ini b/variants/nano-g2-ultra/platformio.ini new file mode 100644 index 0000000..913b38e --- /dev/null +++ b/variants/nano-g2-ultra/platformio.ini @@ -0,0 +1,13 @@ +; First prototype eink/nrf52840/sx1262 device +[env:nano-g2-ultra] +extends = nrf52840_base +board = nano-g2-ultra +debug_tool = jlink + +build_flags = ${nrf52840_base.build_flags} -Ivariants/nano-g2-ultra -D NANO_G2_ULTRA + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nano-g2-ultra> +lib_deps = + ${nrf52840_base.lib_deps} + lewisxhe/PCF8563_Library@^1.0.1 +;upload_protocol = fs diff --git a/variants/nano-g2-ultra/variant.cpp b/variants/nano-g2-ultra/variant.cpp new file mode 100644 index 0000000..ce5d008 --- /dev/null +++ b/variants/nano-g2-ultra/variant.cpp @@ -0,0 +1,36 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 - pins 0 and 1 are hardwired for xtal and should never be enabled + 0xff, 0xff, 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, + + // 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 +} \ No newline at end of file diff --git a/variants/nano-g2-ultra/variant.h b/variants/nano-g2-ultra/variant.h new file mode 100644 index 0000000..fd51cf9 --- /dev/null +++ b/variants/nano-g2-ultra/variant.h @@ -0,0 +1,188 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_Nano_G2_ +#define _VARIANT_Nano_G2_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// #define USE_LFRC // Board uses 32khz RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (-1) +#define PIN_LED2 (-1) +#define PIN_LED3 (-1) + +#define LED_RED PIN_LED3 +#define LED_BLUE PIN_LED1 +#define LED_GREEN PIN_LED2 + +#define LED_BUILTIN LED_BLUE +#define LED_CONN PIN_GREEN + +#define LED_STATE_ON 0 // State when LED is lit + +/* + * Buttons + */ +#define PIN_BUTTON1 (32 + 6) + +#define EXT_NOTIFY_OUT (0 + 4) // Default pin to use for Ext Notify Module. + +/* + * Analog pins + */ +#define PIN_A4 (0 + 2) // Battery ADC + +#define BATTERY_PIN PIN_A4 + +static const uint8_t A4 = PIN_A4; + +#define ADC_RESOLUTION 14 + +/* + * Serial interfaces + */ +#define PIN_SERIAL2_RX (0 + 22) +#define PIN_SERIAL2_TX (0 + 20) + +/** + Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (0 + 17) +#define PIN_WIRE_SCL (0 + 15) + +#define PIN_RTC_INT (0 + 14) // Interrupt from the PCF8563 RTC + +/* +External serial flash W25Q16JV_IQ +*/ + +// QSPI Pins +#define PIN_QSPI_SCK (0 + 8) +#define PIN_QSPI_CS (32 + 7) +#define PIN_QSPI_IO0 (0 + 6) // MOSI if using two bit interface +#define PIN_QSPI_IO1 (0 + 26) // MISO if using two bit interface +#define PIN_QSPI_IO2 (32 + 4) // WP if using two bit interface (i.e. not used) +#define PIN_QSPI_IO3 (32 + 2) // HOLD if using two bit interface (i.e. not used) + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES W25Q16JV_IQ +#define EXTERNAL_FLASH_USE_QSPI + +/* + * Lora radio + */ + +#define USE_SX1262 +#define SX126X_CS (32 + 13) // FIXME - we really should define LORA_CS instead +#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? +#define SX126X_BUSY (32 + 11) +#define SX126X_RESET (32 + 15) +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// #define LORA_DISABLE_SENDING // Define this to disable transmission for testing (power testing etc...) + +// #undef SX126X_CS + +/* + * GPS pins + */ + +#define GPS_L76K + +#define PIN_GPS_STANDBY (0 + 13) // An output to wake GPS, low means allow sleep, high means force wake STANDBY +#define PIN_GPS_TX (0 + 9) // This is for bits going TOWARDS the CPU +#define PIN_GPS_RX (0 + 10) // This is for bits going TOWARDS the GPS + +// #define GPS_THREAD_INTERVAL 50 + +#define PIN_SERIAL1_RX PIN_GPS_TX +#define PIN_SERIAL1_TX PIN_GPS_RX + +// PCF8563 RTC Module +#define PCF8563_RTC 0x51 + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 1 + +// For LORA, spi 0 +#define PIN_SPI_MISO (32 + 9) +#define PIN_SPI_MOSI (0 + 11) +#define PIN_SPI_SCK (0 + 12) + +// #define PIN_PWR_EN (0 + 6) + +// To debug via the segger JLINK console rather than the CDC-ACM serial device +// #define USE_SEGGER + +// Battery +// The battery sense is hooked to pin A0 (2) +// it is defined in the anlaolgue pin section of this file +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER (2.0F) + +#define HAS_RTC 1 + +/** + OLED Screen Model + */ +#define ARDUINO_ARCH_AVR +#define USE_SH1107_128_64 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif \ No newline at end of file diff --git a/variants/picomputer-s3/pins_arduino.h b/variants/picomputer-s3/pins_arduino.h new file mode 100644 index 0000000..a3d4001 --- /dev/null +++ b/variants/picomputer-s3/pins_arduino.h @@ -0,0 +1,22 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +// The default Wire will be mapped to PMU and RTC +static const uint8_t SDA = 8; +static const uint8_t SCL = 9; + +// Default SPI +static const uint8_t MISO = 39; +static const uint8_t SCK = 21; +static const uint8_t MOSI = 38; +static const uint8_t SS = 40; + +#endif /* Pins_Arduino_h */ \ No newline at end of file diff --git a/variants/picomputer-s3/platformio.ini b/variants/picomputer-s3/platformio.ini new file mode 100644 index 0000000..202cd05 --- /dev/null +++ b/variants/picomputer-s3/platformio.ini @@ -0,0 +1,17 @@ +[env:picomputer-s3] +extends = esp32s3_base +board = bpi_picow_esp32_s3 + +;OpenOCD flash method +;upload_protocol = esp-builtin +;Normal method +upload_protocol = esptool + +build_flags = + ${esp32s3_base.build_flags} + -DPICOMPUTER_S3 + -I variants/picomputer-s3 + +lib_deps = + ${esp32s3_base.lib_deps} + lovyan03/LovyanGFX@^1.1.8 diff --git a/variants/picomputer-s3/variant.h b/variants/picomputer-s3/variant.h new file mode 100644 index 0000000..ff8faa6 --- /dev/null +++ b/variants/picomputer-s3/variant.h @@ -0,0 +1,65 @@ +#undef GPS_RX_PIN +#undef GPS_TX_PIN + +#define BUTTON_PIN 0 + +#define PIN_BUZZER 43 + +#define HAS_WIRE 0 + +#define BATTERY_PIN ADC1_CHANNEL_1_GPIO_NUM // 2 +// A battery voltage measurement pin, voltage divider connected here to measure battery voltage +// ratio of voltage divider = 3.0 (R11=200k, R7=100k) +#define ADC_MULTIPLIER 3.1 // 3.0 with correction of display undervoltage. +#define ADC_CHANNEL ADC1_GPIO2_CHANNEL + +#define USE_RF95 // RFM95/SX127x + +#define LORA_SCK SCK // 21 +#define LORA_MISO MISO // 39 +#define LORA_MOSI MOSI // 38 +#define LORA_CS SS // 40 +#define LORA_RESET RADIOLIB_NC + +// per SX1276_Receive_Interrupt/utilities.h +#define LORA_DIO0 10 +#define LORA_DIO1 RADIOLIB_NC +#define LORA_DIO2 RADIOLIB_NC + +// Default SPI1 will be mapped to the display +#define ST7789_SDA 4 +#define ST7789_SCK 3 +#define ST7789_CS 6 +#define ST7789_RS 1 +#define ST7789_BL 5 + +#define ST7789_RESET -1 +#define ST7789_MISO -1 +#define ST7789_BUSY -1 +#define ST7789_SPI_HOST SPI3_HOST +#define TFT_BL 5 +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 16000000 +#define TFT_HEIGHT 320 +#define TFT_WIDTH 240 +#define TFT_OFFSET_X 0 +#define TFT_OFFSET_Y 0 +#define TFT_OFFSET_ROTATION 0 +#define SCREEN_ROTATE +#define SCREEN_TRANSITION_FRAMERATE 5 + +// Picomputer gets a white on black display +#define TFT_MESH COLOR565(0xFF, 0xFF, 0xFF) + +#define CANNED_MESSAGE_MODULE_ENABLE 1 + +#define INPUTBROKER_MATRIX_TYPE 1 + +#define KEYS_COLS \ + { \ + 44, 47, 17, 15, 13, 41 \ + } +#define KEYS_ROWS \ + { \ + 12, 16, 42, 18, 14, 7 \ + } diff --git a/variants/portduino/platformio.ini b/variants/portduino/platformio.ini new file mode 100644 index 0000000..46417e3 --- /dev/null +++ b/variants/portduino/platformio.ini @@ -0,0 +1,10 @@ +[env:native] +extends = portduino_base +; The pkg-config commands below optionally add link flags. +; the || : is just a "or run the null command" to avoid returning an error code +build_flags = ${portduino_base.build_flags} -O0 -I variants/portduino -I /usr/include + !pkg-config --libs libulfius --silence-errors || : + !pkg-config --libs openssl --silence-errors || : +board = cross_platform +lib_deps = ${portduino_base.lib_deps} +build_src_filter = ${portduino_base.build_src_filter} diff --git a/variants/portduino/variant.h b/variants/portduino/variant.h new file mode 100644 index 0000000..5c8f2e1 --- /dev/null +++ b/variants/portduino/variant.h @@ -0,0 +1,8 @@ +#define HAS_SCREEN 0 +#define HAS_RADIO 1 +#define CANNED_MESSAGE_MODULE_ENABLE 1 +#define HAS_GPS 0 +#define MAX_RX_TOPHONE settingsMap[maxtophone] +#define MAX_NUM_NODES settingsMap[maxnodes] +#define RADIOLIB_GODMODE 1 +#define ARCH_PORTDUINO 1 \ No newline at end of file diff --git a/variants/radiomaster_900_bandit/platformio.ini b/variants/radiomaster_900_bandit/platformio.ini new file mode 100644 index 0000000..4ff8a6e --- /dev/null +++ b/variants/radiomaster_900_bandit/platformio.ini @@ -0,0 +1,14 @@ +[env:radiomaster_900_bandit] +extends = esp32_base +board = esp32doit-devkit-v1 +build_flags = + ${esp32_base.build_flags} + -DRADIOMASTER_900_BANDIT + -DVTABLES_IN_FLASH=1 + -DCONFIG_DISABLE_HAL_LOCKS=1 + -O2 + -Ivariants/radiomaster_900_bandit +board_build.f_cpu = 240000000L +upload_protocol = esptool +lib_deps = + ${esp32_base.lib_deps} \ No newline at end of file diff --git a/variants/radiomaster_900_bandit/variant.h b/variants/radiomaster_900_bandit/variant.h new file mode 100644 index 0000000..0c7417c --- /dev/null +++ b/variants/radiomaster_900_bandit/variant.h @@ -0,0 +1,126 @@ +/* + Initial settings and work by https://github.com/gjelsoe + Unit provided by Radio Master RC + https://radiomasterrc.com/products/bandit-expresslrs-rf-module with 1.29" OLED display CH1115 driver +*/ + +/* + On this model then screen is NOT upside down, don't flip it for the user. +*/ +#undef DISPLAY_FLIP_SCREEN + +/* + I2C SDA and SCL. + 0x18 - STK8XXX Accelerometer + 0x3C - SH1115 Display Driver +*/ +#define I2C_SDA 14 +#define I2C_SCL 12 + +/* + I2C STK8XXX Accelerometer Interrupt PIN to ESP32 Pin 6 - SENSOR_CAPP (GPIO37) +*/ +#define STK8XXX_INT 37 + +/* + No GPS - but free pins are available. +*/ +#define HAS_GPS 0 +#undef GPS_RX_PIN +#undef GPS_TX_PIN + +/* + Pin connections from ESP32-D0WDQ6 to SX1276. +*/ +#define LORA_DIO0 22 +#define LORA_DIO1 21 +#define LORA_SCK 18 +#define LORA_MISO 19 +#define LORA_MOSI 23 +#define LORA_CS 4 +#define LORA_RESET 5 +#define LORA_TXEN 33 + +/* + This unit has a FAN built-in. + FAN is active at 250mW on it's ExpressLRS Firmware. + This FAN has TACHO signal on Pin 27 for use with PWM. +*/ +#define RF95_FAN_EN 2 + +/* + LED PIN setup and it has a NeoPixel LED. + It's possible to setup colors for Button 1 and 2, + look at BUTTON1_COLOR, BUTTON1_COLOR_INDEX, BUTTON2_COLOR and BUTTON2_COLOR_INDEX + this is done here for now. +*/ +#define HAS_NEOPIXEL // Enable the use of neopixels +#define NEOPIXEL_COUNT 6 // How many neopixels are connected +#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 + +/* + It has 1 x five-way and 2 x normal buttons. + + Button GPIO RGB Index + --------------------------- + Five-way 39 - + Button 1 34 0 + Button 2 35 1 + + Five way button when using ADC. + 2.632V, 2.177V, 1.598V, 1.055V, 0V + + ADC Values: + { UP, DOWN, LEFT, RIGHT, ENTER, IDLE } + 3227, 0 ,1961, 2668, 1290, 4095 + + Five way button when using ADC. + https://github.com/ExpressLRS/targets/blob/f3215b5ec891108db1a13523e4163950cfcadaac/TX/Radiomaster%20Bandit.json#L41 + +*/ +#define INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE +#define PIN_JOYSTICK 39 +#define JOYSTICK_ADC_VALS /*UP*/ 3227, /*DOWN*/ 0, /*LEFT*/ 1961, /*RIGHT*/ 2668, /*OK*/ 1290, /*IDLE*/ 4095 + +/* + Normal Button Pin setup. +*/ +#define BUTTON_PIN 34 +#define BUTTON_NEED_PULLUP + +/* + No External notification. +*/ +#undef EXT_NOTIFY_OUT + +/* + Remapping PIN Names. + Note, that this unit uses RFO +*/ +#define USE_RF95 +#define USE_RF95_RFO +#define RF95_CS LORA_CS +#define RF95_DIO1 LORA_DIO1 +#define RF95_TXEN LORA_TXEN +#define RF95_RESET LORA_RESET +#define RF95_MAX_POWER 10 + +/* + This module has Skyworks SKY66122 controlled by dacWrite + power ranging from 100mW to 1000mW. + + Mapping of PA_LEVEL to Power output: GPIO26/dacWrite + 168 -> 100mW + 155 -> 250mW + 142 -> 500mW + 110 -> 1000mW +*/ +#define RF95_PA_EN 26 +#define RF95_PA_DAC_EN +#define RF95_PA_LEVEL 110 \ No newline at end of file diff --git a/variants/radiomaster_900_bandit_micro/platformio.ini b/variants/radiomaster_900_bandit_micro/platformio.ini new file mode 100644 index 0000000..9e54f58 --- /dev/null +++ b/variants/radiomaster_900_bandit_micro/platformio.ini @@ -0,0 +1,19 @@ +; +; This uses the same code and settings as the Radio Master Bandit Nano (https://www.radiomasterrc.com/products/bandit-nano-expresslrs-rf-module) +; +; Link to the unit : https://www.radiomasterrc.com/products/bandit-micro-expresslrs-rf-module +; +[env:radiomaster_900_bandit_micro] +extends = esp32_base +board = esp32doit-devkit-v1 +build_flags = + ${esp32_base.build_flags} + -DRADIOMASTER_900_BANDIT_NANO + -DVTABLES_IN_FLASH=1 + -DCONFIG_DISABLE_HAL_LOCKS=1 + -O2 + -Ivariants/radiomaster_900_bandit_nano +board_build.f_cpu = 240000000L +upload_protocol = esptool +lib_deps = + ${esp32_base.lib_deps} \ No newline at end of file diff --git a/variants/radiomaster_900_bandit_nano/platformio.ini b/variants/radiomaster_900_bandit_nano/platformio.ini new file mode 100644 index 0000000..0d43b86 --- /dev/null +++ b/variants/radiomaster_900_bandit_nano/platformio.ini @@ -0,0 +1,14 @@ +[env:radiomaster_900_bandit_nano] +extends = esp32_base +board = esp32doit-devkit-v1 +build_flags = + ${esp32_base.build_flags} + -DRADIOMASTER_900_BANDIT_NANO + -DVTABLES_IN_FLASH=1 + -DCONFIG_DISABLE_HAL_LOCKS=1 + -O2 + -Ivariants/radiomaster_900_bandit_nano +board_build.f_cpu = 240000000L +upload_protocol = esptool +lib_deps = + ${esp32_base.lib_deps} \ No newline at end of file diff --git a/variants/radiomaster_900_bandit_nano/variant.h b/variants/radiomaster_900_bandit_nano/variant.h new file mode 100644 index 0000000..1b6bba1 --- /dev/null +++ b/variants/radiomaster_900_bandit_nano/variant.h @@ -0,0 +1,81 @@ +/* + Initial settings and work by https://github.com/uberhalit and re-work by https://github.com/gjelsoe + Unit provided by Radio Master RC + https://radiomasterrc.com/products/bandit-nano-expresslrs-rf-module with 0.96" OLED display +*/ + +/* + I2C SDA and SCL. +*/ +#define I2C_SDA 14 +#define I2C_SCL 12 + +/* + No GPS - but free solder pads are available inside the case. +*/ +#undef GPS_RX_PIN +#undef GPS_TX_PIN + +/* + Pin connections from ESP32-D0WDQ6 to SX1276. +*/ +#define LORA_DIO0 22 +#define LORA_DIO1 21 +#define LORA_SCK 18 +#define LORA_MISO 19 +#define LORA_MOSI 23 +#define LORA_CS 4 +#define LORA_RESET 5 +#define LORA_TXEN 33 + +/* + This unit has a FAN built-in. + FAN is active at 250mW on it's ExpressLRS Firmware. +*/ +#define RF95_FAN_EN 2 + +/* + LED PIN setup. +*/ +#define LED_PIN 15 + +/* + Five way button when using ADC. + https://github.com/ExpressLRS/targets/blob/f3215b5ec891108db1a13523e4163950cfcadaac/TX/Radiomaster%20Bandit.json#L41 +*/ +#define INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE +#define PIN_JOYSTICK 39 +#define JOYSTICK_ADC_VALS /*UP*/ 3227, /*DOWN*/ 0, /*LEFT*/ 1961, /*RIGHT*/ 2668, /*OK*/ 1290, /*IDLE*/ 4095 + +#define DISPLAY_FLIP_SCREEN + +/* + No External notification. +*/ +#undef EXT_NOTIFY_OUT + +/* + Remapping PIN Names. + Note, that this unit uses RFO +*/ +#define USE_RF95 +#define USE_RF95_RFO +#define RF95_CS LORA_CS +#define RF95_DIO1 LORA_DIO1 +#define RF95_TXEN LORA_TXEN +#define RF95_RESET LORA_RESET +#define RF95_MAX_POWER 12 + +/* + This module has Skyworks SKY66122 controlled by dacWrite + power rangeing from 100mW to 1000mW. + + Mapping of PA_LEVEL to Power output: GPIO26/dacWrite + 168 -> 100mW -> 2.11v + 148 -> 250mW -> 1.87v + 128 -> 500mW -> 1.63v + 90 -> 1000mW -> 1.16v +*/ +#define RF95_PA_EN 26 +#define RF95_PA_DAC_EN +#define RF95_PA_LEVEL 90 diff --git a/variants/rak10701/platformio.ini b/variants/rak10701/platformio.ini new file mode 100644 index 0000000..4c9bf3b --- /dev/null +++ b/variants/rak10701/platformio.ini @@ -0,0 +1,24 @@ +; The very slick RAK wireless RAK10701 Field Tester device. Note you will have to flash to Arduino bootloader to use this firmware. Be aware touch is not currently working. +[env:rak10701] +extends = nrf52840_base +board_level = extra +board = wiscore_rak4631 +build_flags = ${nrf52840_base.build_flags} -Ivariants/rak10701 -D RAK_4631 + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + -DEINK_DISPLAY_MODEL=GxEPD2_213_BN + -DEINK_WIDTH=250 + -DEINK_HEIGHT=122 +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak10701> + + + +lib_deps = + ${nrf52840_base.lib_deps} + ${networking_base.lib_deps} + melopero/Melopero RV3028@^1.1.0 + https://github.com/RAKWireless/RAK13800-W5100S.git#1.0.2 + rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 + bodmer/TFT_eSPI + beegee-tokyo/RAKwireless RAK12034@^1.0.0 + beegee-tokyo/RAK14014-FT6336U @ 1.0.1 +debug_tool = jlink +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +;upload_protocol = jlink \ No newline at end of file diff --git a/variants/rak10701/variant.cpp b/variants/rak10701/variant.cpp new file mode 100644 index 0000000..5a35879 --- /dev/null +++ b/variants/rak10701/variant.cpp @@ -0,0 +1,45 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 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, + + // 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); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); +} \ No newline at end of file diff --git a/variants/rak10701/variant.h b/variants/rak10701/variant.h new file mode 100644 index 0000000..c263796 --- /dev/null +++ b/variants/rak10701/variant.h @@ -0,0 +1,319 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_RAK4630_ +#define _VARIANT_RAK4630_ + +#define RAK4630 + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (35) +#define PIN_LED2 (36) + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 + +#define LED_STATE_ON 1 // State when LED is litted + +/* + * Buttons + */ + +#define PIN_BUTTON1 9 // Pin for button on E-ink button module or IO expansion such as the RAK14014 or RAK14015 TFT modules +#define BUTTON_NEED_PULLUP +#define PIN_BUTTON2 12 +#define PIN_BUTTON3 24 +#define PIN_BUTTON4 25 + +/* + * Analog pins + */ +#define PIN_A0 (5) +#define PIN_A1 (31) +#define PIN_A2 (28) +#define PIN_A3 (29) +#define PIN_A4 (30) +#define PIN_A5 (31) +#define PIN_A6 (0xff) +#define PIN_A7 (0xff) + +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; +static const uint8_t A3 = PIN_A3; +static const uint8_t A4 = PIN_A4; +static const uint8_t A5 = PIN_A5; +static const uint8_t A6 = PIN_A6; +static const uint8_t A7 = PIN_A7; +#define ADC_RESOLUTION 14 + +// Other pins +#define PIN_AREF (2) +#define PIN_NFC1 (9) +#define PIN_NFC2 (10) + +static const uint8_t AREF = PIN_AREF; + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (15) +#define PIN_SERIAL1_TX (16) + +// Connected to Jlink CDC +#define PIN_SERIAL2_RX (8) +#define PIN_SERIAL2_TX (6) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +#define PIN_SPI_MISO (45) +#define PIN_SPI_MOSI (44) +#define PIN_SPI_SCK (43) + +#define PIN_SPI1_MISO (29) // (0 + 29) +#define PIN_SPI1_MOSI (30) // (0 + 30) +#define PIN_SPI1_SCK (3) // (0 + 3) + +static const uint8_t SS = 42; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +/* + * eink display pins + */ + +#define PIN_EINK_CS (0 + 26) +#define PIN_EINK_BUSY (0 + 4) +#define PIN_EINK_DC (0 + 17) +#define PIN_EINK_RES (-1) +#define PIN_EINK_SCLK (0 + 3) +#define PIN_EINK_MOSI (0 + 30) // also called SDI + +// #define USE_EINK + +// RAKRGB +#define HAS_NCP5623 + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (13) +#define PIN_WIRE_SCL (14) + +// QSPI Pins +#define PIN_QSPI_SCK 3 +#define PIN_QSPI_CS 26 +#define PIN_QSPI_IO0 30 +#define PIN_QSPI_IO1 29 +#define PIN_QSPI_IO2 28 +#define PIN_QSPI_IO3 2 + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES IS25LP080D +#define EXTERNAL_FLASH_USE_QSPI + +/* @note RAK5005-O GPIO mapping to RAK4631 GPIO ports + RAK5005-O <-> nRF52840 + IO1 <-> P0.17 (Arduino GPIO number 17) + IO2 <-> P1.02 (Arduino GPIO number 34) + IO3 <-> P0.21 (Arduino GPIO number 21) + IO4 <-> P0.04 (Arduino GPIO number 4) + IO5 <-> P0.09 (Arduino GPIO number 9) + IO6 <-> P0.10 (Arduino GPIO number 10) + IO7 <-> P0.28 (Arduino GPIO number 28) + SW1 <-> P0.01 (Arduino GPIO number 1) + A0 <-> P0.04/AIN2 (Arduino Analog A2 + A1 <-> P0.31/AIN7 (Arduino Analog A7 + SPI_CS <-> P0.26 (Arduino GPIO number 26) + */ + +// No reason not to have the RAK Wireless pin defs here too. This allows code from example RAK sketches to run without +// modification. + +static const uint8_t WB_IO1 = 17; // SLOT_A SLOT_B +static const uint8_t WB_IO2 = 34; // SLOT_A SLOT_B +static const uint8_t WB_IO3 = 21; // SLOT_C +static const uint8_t WB_IO4 = 4; // SLOT_C +static const uint8_t WB_IO5 = 9; // SLOT_D +static const uint8_t WB_IO6 = 10; // SLOT_D +static const uint8_t WB_SW1 = 33; // IO_SLOT +static const uint8_t WB_A0 = 5; // IO_SLOT +static const uint8_t WB_A1 = 31; // IO_SLOT +static const uint8_t WB_I2C1_SDA = 13; // SENSOR_SLOT IO_SLOT +static const uint8_t WB_I2C1_SCL = 14; // SENSOR_SLOT IO_SLOT +static const uint8_t WB_I2C2_SDA = 24; // IO_SLOT +static const uint8_t WB_I2C2_SCL = 25; // IO_SLOT +static const uint8_t WB_SPI_CS = 26; // IO_SLOT +static const uint8_t WB_SPI_CLK = 3; // IO_SLOT +static const uint8_t WB_SPI_MISO = 29; // IO_SLOT +static const uint8_t WB_SPI_MOSI = 30; // IO_SLOT + +// RAK4630 LoRa module + +/* Setup of the SX1262 LoRa module ( https://docs.rakwireless.com/Product-Categories/WisBlock/RAK4631/Datasheet/ ) + +P1.10 NSS SPI NSS (Arduino GPIO number 42) +P1.11 SCK SPI CLK (Arduino GPIO number 43) +P1.12 MOSI SPI MOSI (Arduino GPIO number 44) +P1.13 MISO SPI MISO (Arduino GPIO number 45) +P1.14 BUSY BUSY signal (Arduino GPIO number 46) +P1.15 DIO1 DIO1 event interrupt (Arduino GPIO number 47) +P1.06 NRESET NRESET manual reset of the SX1262 (Arduino GPIO number 38) + +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 + +SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG + +*/ + +#define USE_SX1262 +#define SX126X_CS (42) +#define SX126X_DIO1 (47) +#define SX126X_BUSY (46) +#define SX126X_RESET (38) +// #define SX126X_TXEN (39) +// #define SX126X_RXEN (37) +#define SX126X_POWER_EN (37) +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// Testing USB detection +#define NRF_APM + +// enables 3.3V periphery like GPS or IO Module +#define PIN_3V3_EN (34) + +// 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 + +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +// Define pin to enable GPS toggle (set GPIO to LOW) via user button triple press + +// RAK12002 RTC Module +#define RV3028_RTC (uint8_t)0b1010010 + +// RAK18001 Buzzer in Slot C +// #define PIN_BUZZER 21 // IO3 is PWM2 +// NEW: set this via protobuf instead! + +// Battery +// The battery sense is hooked to pin A0 (5) +#define BATTERY_PIN PIN_A0 +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER (1.73F) + +#define HAS_RTC 1 + +#define HAS_ETHERNET 1 + +#define RAK_4631 1 + +#define PIN_ETHERNET_RESET 21 +#define PIN_ETHERNET_SS PIN_EINK_CS +#define ETH_SPI_PORT SPI1 +#define AQ_SET_PIN 10 + +#ifdef __cplusplus +} +#endif + +#define RAK14014 // Tell it we have a RAK14014 +#define USER_SETUP_LOADED 1 +#define DISABLE_ALL_LIBRARY_WARNINGS 1 +#define ST7789_DRIVER 1 +#define TFT_WIDTH 240 +#define TFT_HEIGHT 320 +#define TFT_MISO WB_SPI_MISO +#define TFT_MOSI WB_SPI_MOSI +#define TFT_SCLK WB_SPI_CLK +#define TFT_CS WB_SPI_CS +#define TFT_DC WB_IO4 +#define TFT_RST -1 +#define TFT_BL WB_IO3 +#define LOAD_GLCD 1 +#define LOAD_GFXFF 1 +#define TFT_RGB_ORDER 0 +#define SPI_FREQUENCY 50000000 +#define TFT_SPI_PORT SPI1 +#define ST7789_CS WB_SPI_CS // Adds compatibility with the rest of the checking for a ST7789 TFT. + +#define SCREEN_ROTATE +#define SCREEN_TRANSITION_FRAMERATE 5 + +#define HAS_TOUCHSCREEN 1 +#define SCREEN_TOUCH_INT WB_IO6 + +#define CANNED_MESSAGE_MODULE_ENABLE 1 + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif \ No newline at end of file diff --git a/variants/rak11200/pins_arduino.h b/variants/rak11200/pins_arduino.h new file mode 100644 index 0000000..f383d54 --- /dev/null +++ b/variants/rak11200/pins_arduino.h @@ -0,0 +1,38 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define LED_GREEN 12 +#define LED_BLUE 2 + +#define LED_BUILTIN LED_GREEN + +static const uint8_t TX = 1; +static const uint8_t RX = 3; + +#define TX1 21 +#define RX1 19 + +#define WB_IO1 14 +#define WB_IO2 27 +#define WB_IO3 26 +#define WB_IO4 23 +#define WB_IO5 13 +#define WB_IO6 22 +#define WB_SW1 34 +#define WB_A0 36 +#define WB_A1 39 +#define WB_CS 32 +#define WB_LED1 12 +#define WB_LED2 2 + +static const uint8_t SDA = 4; +static const uint8_t SCL = 5; + +static const uint8_t SS = 32; +static const uint8_t MOSI = 25; +static const uint8_t MISO = 35; +static const uint8_t SCK = 33; + +#endif /* Pins_Arduino_h */ diff --git a/variants/rak11200/platformio.ini b/variants/rak11200/platformio.ini new file mode 100644 index 0000000..eddc345 --- /dev/null +++ b/variants/rak11200/platformio.ini @@ -0,0 +1,7 @@ +[env:rak11200] +extends = esp32_base +board = wiscore_rak11200 +board_check = true +build_flags = + ${esp32_base.build_flags} -D RAK_11200 -I variants/rak11200 +upload_speed = 115200 \ No newline at end of file diff --git a/variants/rak11200/variant.h b/variants/rak11200/variant.h new file mode 100644 index 0000000..01edb8b --- /dev/null +++ b/variants/rak11200/variant.h @@ -0,0 +1,81 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define LED_GREEN 12 +#define LED_BLUE 2 + +#define LED_BUILTIN LED_GREEN + +static const uint8_t TX = 1; +static const uint8_t RX = 3; + +#define TX1 21 +#define RX1 19 + +#define WB_IO1 14 +#define WB_IO2 27 +#define WB_IO3 26 +#define WB_IO4 23 +#define WB_IO5 13 +#define WB_IO6 22 +#define WB_SW1 34 +#define WB_A0 36 +#define WB_A1 39 +#define WB_CS 32 +#define WB_LED1 12 +#define WB_LED2 2 + +static const uint8_t SDA = 4; +static const uint8_t SCL = 5; + +static const uint8_t SS = 32; +static const uint8_t MOSI = 25; +static const uint8_t MISO = 35; +static const uint8_t SCK = 33; +#endif /* Pins_Arduino_h */ + +/* -------- Meshtastic pins -------- */ +#define I2C_SDA SDA +#define I2C_SCL SCL + +#undef GPS_RX_PIN +#define GPS_RX_PIN (RX1) +#undef GPS_TX_PIN +#define GPS_TX_PIN (TX1) + +#define LED_PIN LED_BLUE + +#define PIN_VBAT WB_A0 +#define BATTERY_PIN PIN_VBAT +#define ADC_CHANNEL ADC1_GPIO36_CHANNEL + +// https://docs.rakwireless.com/Product-Categories/WisBlock/RAK13300/ + +#define LORA_DIO0 RADIOLIB_NC // a No connect on the SX1262/SX1268 module +#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 + +#undef LORA_SCK +#define LORA_SCK SCK +#undef LORA_MISO +#define LORA_MISO MISO +#undef LORA_MOSI +#define LORA_MOSI MOSI +#undef LORA_CS +#define LORA_CS SS + +#define USE_SX1262 +#define SX126X_ANT_SW WB_IO3 +#define SX126X_CS SS // NSS for SX126X +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_POWER_EN WB_IO2 +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 diff --git a/variants/rak11310/pins_arduino.h b/variants/rak11310/pins_arduino.h new file mode 100644 index 0000000..626bed1 --- /dev/null +++ b/variants/rak11310/pins_arduino.h @@ -0,0 +1,68 @@ +#pragma once + +// Pin definitions taken from: +// https://datasheets.raspberrypi.org/pico/pico-datasheet.pdf + +static const uint8_t WB_IO1 = 6; // SLOT_A SLOT_B +static const uint8_t WB_IO2 = 22; // SLOT_A SLOT_B +static const uint8_t WB_IO3 = 7; // SLOT_C +static const uint8_t WB_IO4 = 28; // SLOT_C +static const uint8_t WB_IO5 = 9; // SLOT_D +static const uint8_t WB_IO6 = 8; // SLOT_D +static const uint8_t WB_A0 = 26; // IO_SLOT +static const uint8_t WB_A1 = 27; // IO_SLOT + +#define PIN_A0 (26u) +#define PIN_A1 (27u) +#define PIN_A2 (28u) +#define PIN_A3 (29u) + +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; +static const uint8_t A3 = PIN_A3; + +// LEDs +#define PIN_LED (23u) +#define PIN_LED1 PIN_LED +#define PIN_LED2 (24u) +#define LED_BUILTIN PIN_LED + +#define ADC_RESOLUTION 12 + +// Serial +#define PIN_SERIAL1_TX (0ul) +#define PIN_SERIAL1_RX (1ul) + +#define PIN_SERIAL2_TX (4ul) +#define PIN_SERIAL2_RX (5ul) + +// SPI +#define PIN_SPI0_MISO (12u) +#define PIN_SPI0_MOSI (11u) +#define PIN_SPI0_SCK (10u) +#define PIN_SPI0_SS (13u) + +#define PIN_SPI1_MISO (16u) +#define PIN_SPI1_MOSI (19u) +#define PIN_SPI1_SCK (18u) +#define PIN_SPI1_SS (17u) + +// Wire +#define PIN_WIRE0_SDA (2u) +#define PIN_WIRE0_SCL (3u) + +#define PIN_WIRE1_SDA (20u) +#define PIN_WIRE1_SCL (21u) + +#define SERIAL_HOWMANY (3u) +#define SPI_HOWMANY (2u) +#define WIRE_HOWMANY (2u) + +static const uint8_t SS = PIN_SPI0_SS; +static const uint8_t MOSI = PIN_SPI0_MOSI; +static const uint8_t MISO = PIN_SPI0_MISO; +static const uint8_t SCK = PIN_SPI0_SCK; + +static const uint8_t SDA = PIN_WIRE0_SDA; +static const uint8_t SCL = PIN_WIRE0_SCL; \ No newline at end of file diff --git a/variants/rak11310/platformio.ini b/variants/rak11310/platformio.ini new file mode 100644 index 0000000..c7b3504 --- /dev/null +++ b/variants/rak11310/platformio.ini @@ -0,0 +1,17 @@ +[env:rak11310] +extends = rp2040_base +board = wiscore_rak11300 +upload_protocol = picotool +# keep an old SDK to use less memory. +platform_packages = framework-arduinopico@https://github.com/earlephilhower/arduino-pico.git#3.7.2 + +# add our variants files to the include and src paths +build_flags = ${rp2040_base.build_flags} + -DRAK11310 + -Ivariants/rak11310 + -DDEBUG_RP2040_PORT=Serial + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m0plus" +lib_deps = + ${rp2040_base.lib_deps} +debug_build_flags = ${rp2040_base.build_flags} +debug_tool = cmsis-dap ; for e.g. Picotool \ No newline at end of file diff --git a/variants/rak11310/variant.h b/variants/rak11310/variant.h new file mode 100644 index 0000000..54e403e --- /dev/null +++ b/variants/rak11310/variant.h @@ -0,0 +1,51 @@ +// #define RADIOLIB_CUSTOM_ARDUINO 1 +// #define RADIOLIB_TONE_UNSUPPORTED 1 +// #define RADIOLIB_SOFTWARE_SERIAL_UNSUPPORTED 1 + +#define ARDUINO_ARCH_AVR + +#define LED_CONN PIN_LED2 +#define LED_PIN LED_BUILTIN +#define ledOff(pin) pinMode(pin, INPUT) + +#define BUTTON_PIN 9 +#define BUTTON_NEED_PULLUP +// #define EXT_NOTIFY_OUT 4 + +#define BATTERY_PIN 26 +#define BATTERY_SENSE_RESOLUTION_BITS ADC_RESOLUTION +// ratio of voltage divider = 3.0 (R17=200k, R18=100k) +#define ADC_MULTIPLIER 1.84 + +#define DETECTION_SENSOR_EN 28 + +#define USE_SX1262 + +#undef LORA_SCK +#undef LORA_MISO +#undef LORA_MOSI +#undef LORA_CS + +// RAK BSP somehow uses SPI1 instead of SPI0 +#define HW_SPI1_DEVICE +#define LORA_SCK PIN_SPI0_SCK +#define LORA_MOSI PIN_SPI0_MOSI +#define LORA_MISO PIN_SPI0_MISO +#define LORA_CS PIN_SPI0_SS + +#define LORA_DIO0 RADIOLIB_NC +#define LORA_RESET 14 +#define LORA_DIO1 29 +#define LORA_DIO2 15 +#define LORA_DIO3 RADIOLIB_NC + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_POWER_EN 25 +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif diff --git a/variants/rak2560/RAK9154Sensor.cpp b/variants/rak2560/RAK9154Sensor.cpp new file mode 100644 index 0000000..9f66094 --- /dev/null +++ b/variants/rak2560/RAK9154Sensor.cpp @@ -0,0 +1,183 @@ +#ifdef HAS_RAKPROT +#include "../variants/rak2560/RAK9154Sensor.h" +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "../modules/Telemetry/Sensor/TelemetrySensor.h" +#include "configuration.h" + +#include "concurrency/Periodic.h" +#include + +using namespace concurrency; + +#define BOOT_DATA_REQ + +RAK9154Sensor::RAK9154Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SENSOR_UNSET, "RAK1954") {} + +static Periodic *onewirePeriodic; + +static SoftwareHalfSerial mySerial(HALF_UART_PIN); // Wire pin P0.15 + +static uint8_t buff[0x100]; +static uint16_t bufflen = 0; + +static int16_t dc_cur = 0; +static uint16_t dc_vol = 0; +static uint8_t dc_prec = 0; +static uint8_t provision = 0; + +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_ADD_SID: + // LOG_INFO("+ADD:SID:[%02x]\r\n", msg[0]); + break; + + case SNHUBAPI_EVT_ADD_PID: + // LOG_INFO("+ADD:PID:[%02x]\r\n", msg[0]); +#ifdef BOOT_DATA_REQ + provision = msg[0]; +#endif + break; + + case SNHUBAPI_EVT_GET_INTV: + break; + + case SNHUBAPI_EVT_GET_ENABLE: + break; + + case SNHUBAPI_EVT_SDATA_REQ: + + // LOG_INFO("+EVT:PID[%02x],IPSO[%02x]\r\n",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; + case SNHUBAPI_EVT_REPORT: + + // LOG_INFO("+EVT:PID[%02x],IPSO[%02x]\r\n",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; + } + + break; + + case SNHUBAPI_EVT_CHKSUM_ERR: + LOG_INFO("+ERR:CHKSUM\r\n"); + break; + + case SNHUBAPI_EVT_SEQ_ERR: + LOG_INFO("+ERR:SEQUCE\r\n"); + break; + + default: + break; + } +} + +static int32_t onewireHandle() +{ + if (provision != 0) { + RakSNHub_Protocl_API.get.data(provision); + provision = 0; + } + + while (mySerial.available()) { + char a = mySerial.read(); + buff[bufflen++] = a; + delay(2); // continue data, timeout=2ms + } + + if (bufflen != 0) { + RakSNHub_Protocl_API.process((uint8_t *)buff, bufflen); + bufflen = 0; + } + + return 50; +} + +int32_t RAK9154Sensor::runOnce() +{ + onewirePeriodic = new Periodic("onewireHandle", onewireHandle); + + mySerial.begin(9600); + + RakSNHub_Protocl_API.init(onewire_evt); + + status = true; + initialized = true; + return 0; +} + +void RAK9154Sensor::setup() +{ + // Set up oversampling and filter initialization +} + +bool RAK9154Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + return true; +} + +uint16_t RAK9154Sensor::getBusVoltageMv() +{ + return dc_vol; +} + +int RAK9154Sensor::getBusBatteryPercent() +{ + return (int)dc_prec; +} + +bool RAK9154Sensor::isCharging() +{ + return (dc_cur > 0) ? true : false; +} +#endif // HAS_RAKPROT \ No newline at end of file diff --git a/variants/rak2560/RAK9154Sensor.h b/variants/rak2560/RAK9154Sensor.h new file mode 100644 index 0000000..6c6f304 --- /dev/null +++ b/variants/rak2560/RAK9154Sensor.h @@ -0,0 +1,23 @@ +#ifdef HAS_RAKPROT +#ifndef _RAK9154SENSOR_H +#define _RAK9154SENSOR_H 1 +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "../modules/Telemetry/Sensor/TelemetrySensor.h" +#include "../modules/Telemetry/Sensor/VoltageSensor.h" + +class RAK9154Sensor : public TelemetrySensor, VoltageSensor +{ + private: + protected: + virtual void setup() override; + + public: + RAK9154Sensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual uint16_t getBusVoltageMv() override; + int getBusBatteryPercent(); + bool isCharging(); +}; +#endif // _RAK9154SENSOR_H +#endif // HAS_RAKPROT \ No newline at end of file diff --git a/variants/rak2560/create_uf2.py b/variants/rak2560/create_uf2.py new file mode 100644 index 0000000..af78f3e --- /dev/null +++ b/variants/rak2560/create_uf2.py @@ -0,0 +1,113 @@ +import struct + +Import("env") # noqa: F821 + + +# Parse input and create UF2 file +def create_uf2(source, target, env): + # source_hex = target[0].get_abspath() + source_hex = target[0].get_string(False) + source_hex = ".\\" + source_hex + print("#########################################################") + print("Create UF2 from " + source_hex) + print("#########################################################") + # print("Source: " + source_hex) + target = source_hex.replace(".hex", "") + target = target + ".uf2" + # print("Target: " + target) + + with open(source_hex, mode="rb") as f: + inpbuf = f.read() + + outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8")) + + write_file(target, outbuf) + print("#########################################################") + print(target + " is ready to flash to target device") + print("#########################################################") + + +# Add callback after .hex file was created +env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex", create_uf2) # noqa: F821 + +# UF2 creation taken from uf2conv.py +UF2_MAGIC_START0 = 0x0A324655 # "UF2\n" +UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected +UF2_MAGIC_END = 0x0AB16F30 # Ditto + +familyid = 0xADA52840 + + +class Block: + def __init__(self, addr): + self.addr = addr + self.bytes = bytearray(256) + + def encode(self, blockno, numblocks): + global familyid + flags = 0x0 + if familyid: + flags |= 0x2000 + hd = struct.pack( + " + + + +lib_deps = + ${nrf52840_base.lib_deps} + ${networking_base.lib_deps} + melopero/Melopero RV3028@^1.1.0 + https://github.com/RAKWireless/RAK13800-W5100S.git#1.0.2 + rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 + beegee-tokyo/RAKwireless RAK12034@^1.0.0 + https://github.com/beegee-tokyo/RAK-OneWireSerial.git +debug_tool = jlink +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +;upload_protocol = jlink diff --git a/variants/rak2560/variant.cpp b/variants/rak2560/variant.cpp new file mode 100644 index 0000000..e84b60b --- /dev/null +++ b/variants/rak2560/variant.cpp @@ -0,0 +1,45 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 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, + + // 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); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); +} diff --git a/variants/rak2560/variant.h b/variants/rak2560/variant.h new file mode 100644 index 0000000..8e5d905 --- /dev/null +++ b/variants/rak2560/variant.h @@ -0,0 +1,281 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_RAK2560_ +#define _VARIANT_RAK2560_ + +#define RAK4630 +#define RAK2560 + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (35) +#define PIN_LED2 (36) + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 + +#define LED_STATE_ON 1 // State when LED is litted + +/* + * Buttons + */ + +#define PIN_BUTTON1 9 // Pin for button on E-ink button module or IO expansion +#define BUTTON_NEED_PULLUP +#define PIN_BUTTON2 12 +#define PIN_BUTTON3 24 +#define PIN_BUTTON4 25 + +/* + * Analog pins + */ +#define PIN_A0 (5) +#define PIN_A1 (31) +#define PIN_A2 (28) +#define PIN_A3 (29) +#define PIN_A4 (30) +#define PIN_A5 (31) +#define PIN_A6 (0xff) +#define PIN_A7 (0xff) + +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; +static const uint8_t A3 = PIN_A3; +static const uint8_t A4 = PIN_A4; +static const uint8_t A5 = PIN_A5; +static const uint8_t A6 = PIN_A6; +static const uint8_t A7 = PIN_A7; +#define ADC_RESOLUTION 14 + +// Other pins +#define PIN_AREF (2) +#define PIN_NFC1 (9) +#define PIN_NFC2 (10) + +static const uint8_t AREF = PIN_AREF; + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (15) +#define PIN_SERIAL1_TX (16) + +// Connected to Serial 2 +#define PIN_SERIAL2_RX (19) +#define PIN_SERIAL2_TX (20) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +#define PIN_SPI_MISO (45) +#define PIN_SPI_MOSI (44) +#define PIN_SPI_SCK (43) + +#define PIN_SPI1_MISO (29) // (0 + 29) +#define PIN_SPI1_MOSI (30) // (0 + 30) +#define PIN_SPI1_SCK (3) // (0 + 3) + +static const uint8_t SS = 42; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +/* + * eink display pins + */ + +#define PIN_EINK_CS (0 + 26) +#define PIN_EINK_BUSY (0 + 4) +#define PIN_EINK_DC (0 + 17) +#define PIN_EINK_RES (-1) +#define PIN_EINK_SCLK (0 + 3) +#define PIN_EINK_MOSI (0 + 30) // also called SDI + +// #define USE_EINK + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (13) +#define PIN_WIRE_SCL (14) + +// QSPI Pins +#define PIN_QSPI_SCK 3 +#define PIN_QSPI_CS 26 +#define PIN_QSPI_IO0 30 +#define PIN_QSPI_IO1 29 +#define PIN_QSPI_IO2 28 +#define PIN_QSPI_IO3 2 + +/* @note RAK5005-O GPIO mapping to RAK4631 GPIO ports + RAK5005-O <-> nRF52840 + IO1 <-> P0.17 (Arduino GPIO number 17) + IO2 <-> P1.02 (Arduino GPIO number 34) + IO3 <-> P0.21 (Arduino GPIO number 21) + IO4 <-> P0.04 (Arduino GPIO number 4) + IO5 <-> P0.09 (Arduino GPIO number 9) + IO6 <-> P0.10 (Arduino GPIO number 10) + IO7 <-> P0.28 (Arduino GPIO number 28) + SW1 <-> P0.01 (Arduino GPIO number 1) + A0 <-> P0.04/AIN2 (Arduino Analog A2 + A1 <-> P0.31/AIN7 (Arduino Analog A7 + SPI_CS <-> P0.26 (Arduino GPIO number 26) + */ + +// RAK4630 LoRa module + +/* Setup of the SX1262 LoRa module ( https://docs.rakwireless.com/Product-Categories/WisBlock/RAK4631/Datasheet/ ) + +P1.10 NSS SPI NSS (Arduino GPIO number 42) +P1.11 SCK SPI CLK (Arduino GPIO number 43) +P1.12 MOSI SPI MOSI (Arduino GPIO number 44) +P1.13 MISO SPI MISO (Arduino GPIO number 45) +P1.14 BUSY BUSY signal (Arduino GPIO number 46) +P1.15 DIO1 DIO1 event interrupt (Arduino GPIO number 47) +P1.06 NRESET NRESET manual reset of the SX1262 (Arduino GPIO number 38) + +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 + +SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG + +*/ + +#define DETECTION_SENSOR_EN 4 + +#define USE_SX1262 +#define SX126X_CS (42) +#define SX126X_DIO1 (47) +#define SX126X_BUSY (46) +#define SX126X_RESET (38) +// #define SX126X_TXEN (39) +// #define SX126X_RXEN (37) +#define SX126X_POWER_EN (37) +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// Testing USB detection +#define NRF_APM + +// enables 3.3V periphery like GPS or IO Module +// Do not toggle this for GPS power savings +#define PIN_3V3_EN (34) + +// 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 + +// On RAK2560 the GPS is be on a different UART +// #define GPS_RX_PIN PIN_SERIAL2_RX +// #define GPS_TX_PIN PIN_SERIAL2_TX +// #define PIN_GPS_EN PIN_3V3_EN +// Disable GPS +// #define MESHTASTIC_EXCLUDE_GPS 1 +// Define pin to enable GPS toggle (set GPIO to LOW) via user button triple press + +// RAK12002 RTC Module +#define RV3028_RTC (uint8_t)0b1010010 + +// RAK18001 Buzzer in Slot C +// #define PIN_BUZZER 21 // IO3 is PWM2 +// NEW: set this via protobuf instead! + +// Battery +// The battery sense is hooked to pin A0 (5) +#define BATTERY_PIN PIN_A0 +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER 1.73 + +#define HAS_RTC 1 + +#define HAS_ETHERNET 1 + +#define RAK_4631 1 + +#define HALF_UART_PIN PIN_SERIAL1_RX + +#if defined(GPS_RX_PIN) && (GPS_RX_PIN == HALF_UART_PIN) +#error pin 15 collision + +#endif + +#if defined(GPS_TX_PIN) && (GPS_RX_PIN == HALF_UART_PIN) +#error pin 15 collision +#endif + +#define PIN_ETHERNET_RESET 21 +#define PIN_ETHERNET_SS PIN_EINK_CS +#define ETH_SPI_PORT SPI1 +#define AQ_SET_PIN 10 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif \ No newline at end of file diff --git a/variants/rak3172/platformio.ini b/variants/rak3172/platformio.ini new file mode 100644 index 0000000..9e617e0 --- /dev/null +++ b/variants/rak3172/platformio.ini @@ -0,0 +1,34 @@ +[env:rak3172] +extends = stm32_base +board = wiscore_rak3172 +build_flags = + ${stm32_base.build_flags} + -Ivariants/rak3172 + -DSERIAL_UART_INSTANCE=1 + -DPIN_SERIAL_RX=PB7 + -DPIN_SERIAL_TX=PB6 + -DHAL_DAC_MODULE_ONLY + -DHAL_ADC_MODULE_DISABLED + -DHAL_COMP_MODULE_DISABLED + -DHAL_CRC_MODULE_DISABLED + -DHAL_CRYP_MODULE_DISABLED + -DHAL_GTZC_MODULE_DISABLED + -DHAL_HSEM_MODULE_DISABLED + -DHAL_I2C_MODULE_DISABLED + -DHAL_I2S_MODULE_DISABLED + -DHAL_IPCC_MODULE_DISABLED + -DHAL_IRDA_MODULE_DISABLED + -DHAL_IWDG_MODULE_DISABLED + -DHAL_LPTIM_MODULE_DISABLED + -DHAL_PKA_MODULE_DISABLED + -DHAL_RNG_MODULE_DISABLED + -DHAL_RTC_MODULE_DISABLED + -DHAL_SMARTCARD_MODULE_DISABLED + -DHAL_SMBUS_MODULE_DISABLED + -DHAL_TIM_MODULE_DISABLED + -DHAL_WWDG_MODULE_DISABLED + -DHAL_EXTI_MODULE_DISABLED + -DRADIOLIB_EXCLUDE_SX128X=1 + -DRADIOLIB_EXCLUDE_SX127X=1 + -DRADIOLIB_EXCLUDE_LR11X0=1 +upload_port = stlink \ No newline at end of file diff --git a/variants/rak3172/variant.h b/variants/rak3172/variant.h new file mode 100644 index 0000000..21de65b --- /dev/null +++ b/variants/rak3172/variant.h @@ -0,0 +1,12 @@ +/* +This variant is a work in progress. +Do not expect a working Meshtastic device with this target. +*/ + +#ifndef _VARIANT_RAK3172_ +#define _VARIANT_RAK3172_ + +#define USE_STM32WLx +#define MAX_NUM_NODES 10 + +#endif \ No newline at end of file diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini new file mode 100644 index 0000000..ced93df --- /dev/null +++ b/variants/rak4631/platformio.ini @@ -0,0 +1,55 @@ +; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921 +[env:rak4631] +extends = nrf52840_base +board = wiscore_rak4631 +board_check = true +build_flags = ${nrf52840_base.build_flags} -Ivariants/rak4631 -D RAK_4631 + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + -DEINK_DISPLAY_MODEL=GxEPD2_213_BN + -DEINK_WIDTH=250 + -DEINK_HEIGHT=122 + -DRADIOLIB_EXCLUDE_SX128X=1 + -DRADIOLIB_EXCLUDE_SX127X=1 + -DRADIOLIB_EXCLUDE_LR11X0=1 +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak4631> + + + +lib_deps = + ${nrf52840_base.lib_deps} + ${networking_base.lib_deps} + melopero/Melopero RV3028@^1.1.0 + https://github.com/RAKWireless/RAK13800-W5100S.git#1.0.2 + rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 + https://github.com/RAKWireless/RAK12034-BMX160.git#dcead07ffa267d3c906e9ca4a1330ab989e957e2 + +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +; Note: as of 6/2013 the serial/bootloader based programming takes approximately 30 seconds +;upload_protocol = jlink + +; Allows programming and debug via the RAK NanoDAP as the default debugger tool for the RAK4631 (it is only $10!) +; programming time is about the same as the bootloader version. +; For information on this see the meshtastic developers documentation for "Development on the NRF52" +[env:rak4631_dbg] +extends = env:rak4631 +board_level = extra + +; if the builtin version of openocd has a buggy version of semihosting, so use the external version +; platform_packages = platformio/tool-openocd@^3.1200.0 + +build_flags = + ${env:rak4631.build_flags} + -D USE_SEMIHOSTING + +lib_deps = + ${env:rak4631.lib_deps} + https://github.com/geeksville/Armduino-Semihosting.git#35b538fdf208c3530c1434cd099a08e486672ee4 + +; NOTE: the pyocd support for semihosting is buggy. So I switched to using the builtin platformio support for the stlink adapter which worked much better. +; However the built in openocd version in platformio has buggy support for TCP to semihosting. +; +; So I'm now trying the external openocd - but the openocd scripts for nrf52.cfg assume you are using a DAP adapter not an STLINK adapter. +; In theory I could change those scripts. But for now I'm trying going back to a DAP adapter but with the external openocd. + +upload_protocol = stlink +; eventually use platformio/tool-pyocd@^2.3600.0 instad +;upload_protocol = custom +;upload_command = pyocd flash -t nrf52840 $UPLOADERFLAGS $SOURCE \ No newline at end of file diff --git a/variants/rak4631/variant.cpp b/variants/rak4631/variant.cpp new file mode 100644 index 0000000..e84b60b --- /dev/null +++ b/variants/rak4631/variant.cpp @@ -0,0 +1,45 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 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, + + // 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); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); +} diff --git a/variants/rak4631/variant.h b/variants/rak4631/variant.h new file mode 100644 index 0000000..bc55413 --- /dev/null +++ b/variants/rak4631/variant.h @@ -0,0 +1,273 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_RAK4630_ +#define _VARIANT_RAK4630_ + +#define RAK4630 + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (35) +#define PIN_LED2 (36) + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 + +#define LED_STATE_ON 1 // State when LED is litted + +/* + * Buttons + */ + +#define PIN_BUTTON1 9 // Pin for button on E-ink button module or IO expansion +#define BUTTON_NEED_PULLUP +#define PIN_BUTTON2 12 +#define PIN_BUTTON3 24 +#define PIN_BUTTON4 25 + +/* + * Analog pins + */ +#define PIN_A0 (5) +#define PIN_A1 (31) +#define PIN_A2 (28) +#define PIN_A3 (29) +#define PIN_A4 (30) +#define PIN_A5 (31) +#define PIN_A6 (0xff) +#define PIN_A7 (0xff) + +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; +static const uint8_t A3 = PIN_A3; +static const uint8_t A4 = PIN_A4; +static const uint8_t A5 = PIN_A5; +static const uint8_t A6 = PIN_A6; +static const uint8_t A7 = PIN_A7; +#define ADC_RESOLUTION 14 + +// Other pins +#define PIN_AREF (2) +#define PIN_NFC1 (9) +#define PIN_NFC2 (10) + +static const uint8_t AREF = PIN_AREF; + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (15) +#define PIN_SERIAL1_TX (16) + +// Connected to Jlink CDC +#define PIN_SERIAL2_RX (8) +#define PIN_SERIAL2_TX (6) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +#define PIN_SPI_MISO (45) +#define PIN_SPI_MOSI (44) +#define PIN_SPI_SCK (43) + +#define PIN_SPI1_MISO (29) // (0 + 29) +#define PIN_SPI1_MOSI (30) // (0 + 30) +#define PIN_SPI1_SCK (3) // (0 + 3) + +static const uint8_t SS = 42; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +/* + * eink display pins + */ + +#define PIN_EINK_CS (0 + 26) +#define PIN_EINK_BUSY (0 + 4) +#define PIN_EINK_DC (0 + 17) +#define PIN_EINK_RES (-1) +#define PIN_EINK_SCLK (0 + 3) +#define PIN_EINK_MOSI (0 + 30) // also called SDI + +// #define USE_EINK + +// RAKRGB +#define HAS_NCP5623 + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (13) +#define PIN_WIRE_SCL (14) + +// QSPI Pins +#define PIN_QSPI_SCK 3 +#define PIN_QSPI_CS 26 +#define PIN_QSPI_IO0 30 +#define PIN_QSPI_IO1 29 +#define PIN_QSPI_IO2 28 +#define PIN_QSPI_IO3 2 + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES IS25LP080D +#define EXTERNAL_FLASH_USE_QSPI + +/* @note RAK5005-O GPIO mapping to RAK4631 GPIO ports + RAK5005-O <-> nRF52840 + IO1 <-> P0.17 (Arduino GPIO number 17) + IO2 <-> P1.02 (Arduino GPIO number 34) + IO3 <-> P0.21 (Arduino GPIO number 21) + IO4 <-> P0.04 (Arduino GPIO number 4) + IO5 <-> P0.09 (Arduino GPIO number 9) + IO6 <-> P0.10 (Arduino GPIO number 10) + IO7 <-> P0.28 (Arduino GPIO number 28) + SW1 <-> P0.01 (Arduino GPIO number 1) + A0 <-> P0.04/AIN2 (Arduino Analog A2 + A1 <-> P0.31/AIN7 (Arduino Analog A7 + SPI_CS <-> P0.26 (Arduino GPIO number 26) + */ + +// RAK4630 LoRa module + +/* Setup of the SX1262 LoRa module ( https://docs.rakwireless.com/Product-Categories/WisBlock/RAK4631/Datasheet/ ) + +P1.10 NSS SPI NSS (Arduino GPIO number 42) +P1.11 SCK SPI CLK (Arduino GPIO number 43) +P1.12 MOSI SPI MOSI (Arduino GPIO number 44) +P1.13 MISO SPI MISO (Arduino GPIO number 45) +P1.14 BUSY BUSY signal (Arduino GPIO number 46) +P1.15 DIO1 DIO1 event interrupt (Arduino GPIO number 47) +P1.06 NRESET NRESET manual reset of the SX1262 (Arduino GPIO number 38) + +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 + +SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG + +*/ + +#define DETECTION_SENSOR_EN 4 + +#define USE_SX1262 +#define SX126X_CS (42) +#define SX126X_DIO1 (47) +#define SX126X_BUSY (46) +#define SX126X_RESET (38) +// #define SX126X_TXEN (39) +// #define SX126X_RXEN (37) +#define SX126X_POWER_EN (37) +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// Testing USB detection +#define NRF_APM + +// enables 3.3V periphery like GPS or IO Module +// Do not toggle this for GPS power savings +#define PIN_3V3_EN (34) + +// 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 + +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +// Define pin to enable GPS toggle (set GPIO to LOW) via user button triple press + +// RAK12002 RTC Module +#define RV3028_RTC (uint8_t)0b1010010 + +// RAK18001 Buzzer in Slot C +// #define PIN_BUZZER 21 // IO3 is PWM2 +// NEW: set this via protobuf instead! + +// Battery +// The battery sense is hooked to pin A0 (5) +#define BATTERY_PIN PIN_A0 +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER 1.73 + +#define HAS_RTC 1 + +#define HAS_ETHERNET 1 + +#define RAK_4631 1 + +#define PIN_ETHERNET_RESET 21 +#define PIN_ETHERNET_SS PIN_EINK_CS +#define ETH_SPI_PORT SPI1 +#define AQ_SET_PIN 10 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif \ No newline at end of file diff --git a/variants/rak4631_epaper/platformio.ini b/variants/rak4631_epaper/platformio.ini new file mode 100644 index 0000000..2479f09 --- /dev/null +++ b/variants/rak4631_epaper/platformio.ini @@ -0,0 +1,22 @@ +; The very slick RAK wireless RAK 4631 / 4630 board - Firmware for 5005 with the RAK 14000 ePaper +[env:rak4631_eink] +extends = nrf52840_base +board = wiscore_rak4631 +build_flags = ${nrf52840_base.build_flags} -Ivariants/rak4631_epaper -D RAK_4631 + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" + -DEINK_DISPLAY_MODEL=GxEPD2_213_BN + -DEINK_WIDTH=250 + -DEINK_HEIGHT=122 + -DRADIOLIB_EXCLUDE_SX128X=1 + -DRADIOLIB_EXCLUDE_SX127X=1 + -DRADIOLIB_EXCLUDE_LR11X0=1 +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak4631_epaper> +lib_deps = + ${nrf52840_base.lib_deps} + zinggjm/GxEPD2@^1.4.9 + melopero/Melopero RV3028@^1.1.0 + rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 + beegee-tokyo/RAKwireless RAK12034@^1.0.0 +debug_tool = jlink +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +;upload_protocol = jlink \ No newline at end of file diff --git a/variants/rak4631_epaper/variant.cpp b/variants/rak4631_epaper/variant.cpp new file mode 100644 index 0000000..e84b60b --- /dev/null +++ b/variants/rak4631_epaper/variant.cpp @@ -0,0 +1,45 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 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, + + // 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); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); +} diff --git a/variants/rak4631_epaper/variant.h b/variants/rak4631_epaper/variant.h new file mode 100644 index 0000000..0bb9749 --- /dev/null +++ b/variants/rak4631_epaper/variant.h @@ -0,0 +1,234 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_RAK4630_ +#define _VARIANT_RAK4630_ + +#define RAK4630 + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (35) +#define PIN_LED2 (36) + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 + +#define LED_STATE_ON 1 // State when LED is litted + +/* + * Buttons + */ + +#define PIN_BUTTON1 9 // Pin for button on E-ink button module or IO expansion +#define BUTTON_NEED_PULLUP +#define PIN_BUTTON2 12 +#define PIN_BUTTON3 24 +#define PIN_BUTTON4 25 + +/* + * Analog pins + */ +#define PIN_A0 (5) +#define PIN_A1 (31) +#define PIN_A2 (28) +#define PIN_A3 (29) +#define PIN_A4 (30) +#define PIN_A5 (31) +#define PIN_A6 (0xff) +#define PIN_A7 (0xff) + +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; +static const uint8_t A3 = PIN_A3; +static const uint8_t A4 = PIN_A4; +static const uint8_t A5 = PIN_A5; +static const uint8_t A6 = PIN_A6; +static const uint8_t A7 = PIN_A7; +#define ADC_RESOLUTION 14 + +// Other pins +#define PIN_AREF (2) +#define PIN_NFC1 (9) +#define PIN_NFC2 (10) + +static const uint8_t AREF = PIN_AREF; + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (15) +#define PIN_SERIAL1_TX (16) + +// Connected to Jlink CDC +#define PIN_SERIAL2_RX (8) +#define PIN_SERIAL2_TX (6) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +#define PIN_SPI_MISO (45) +#define PIN_SPI_MOSI (44) +#define PIN_SPI_SCK (43) + +#define PIN_SPI1_MISO (29) // (0 + 29) +#define PIN_SPI1_MOSI (30) // (0 + 30) +#define PIN_SPI1_SCK (3) // (0 + 3) + +static const uint8_t SS = 42; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +/* + * eink display pins + */ + +#define PIN_EINK_CS (0 + 26) +#define PIN_EINK_BUSY (0 + 4) +#define PIN_EINK_DC (0 + 17) +#define PIN_EINK_RES (-1) +#define PIN_EINK_SCLK (0 + 3) +#define PIN_EINK_MOSI (0 + 30) // also called SDI + +#define USE_EINK + +// RAKRGB +#define HAS_NCP5623 + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (13) +#define PIN_WIRE_SCL (14) + +// QSPI Pins +#define PIN_QSPI_SCK 3 +#define PIN_QSPI_CS 26 +#define PIN_QSPI_IO0 30 +#define PIN_QSPI_IO1 29 +#define PIN_QSPI_IO2 28 +#define PIN_QSPI_IO3 2 + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES IS25LP080D +#define EXTERNAL_FLASH_USE_QSPI + +/* @note RAK5005-O GPIO mapping to RAK4631 GPIO ports + RAK5005-O <-> nRF52840 + IO1 <-> P0.17 (Arduino GPIO number 17) + IO2 <-> P1.02 (Arduino GPIO number 34) + IO3 <-> P0.21 (Arduino GPIO number 21) + IO4 <-> P0.04 (Arduino GPIO number 4) + IO5 <-> P0.09 (Arduino GPIO number 9) + IO6 <-> P0.10 (Arduino GPIO number 10) + IO7 <-> P0.28 (Arduino GPIO number 28) + SW1 <-> P0.01 (Arduino GPIO number 1) + A0 <-> P0.04/AIN2 (Arduino Analog A2 + A1 <-> P0.31/AIN7 (Arduino Analog A7 + SPI_CS <-> P0.26 (Arduino GPIO number 26) + */ + +// RAK4630 LoRa module +#define USE_SX1262 +#define SX126X_CS (42) +#define SX126X_DIO1 (47) +#define SX126X_BUSY (46) +#define SX126X_RESET (38) +// #define SX126X_TXEN (39) +// #define SX126X_RXEN (37) +#define SX126X_POWER_EN (37) +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// enables 3.3V periphery like GPS or IO Module +#define PIN_3V3_EN (34) + +// 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 + +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +// RAK12002 RTC Module +#define RV3028_RTC (uint8_t)0b1010010 + +// Testing USB detection +#define NRF_APM + +// Battery +// The battery sense is hooked to pin A0 (5) +#define BATTERY_PIN PIN_A0 +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER 1.73 + +#define HAS_RTC 1 + +#define RAK_4631 1 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif \ No newline at end of file diff --git a/variants/rak4631_epaper_onrxtx/platformio.ini b/variants/rak4631_epaper_onrxtx/platformio.ini new file mode 100644 index 0000000..8c1b8ee --- /dev/null +++ b/variants/rak4631_epaper_onrxtx/platformio.ini @@ -0,0 +1,25 @@ +; The very slick RAK wireless RAK 4631 / 4630 board - Firmware for 5005 with the RAK 14000 ePaper +[env:rak4631_eink_onrxtx] +board_level = extra +extends = nrf52840_base +board = wiscore_rak4631 +build_flags = ${nrf52840_base.build_flags} -Ivariants/rak4631_epaper -D RAK_4631 + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" + -D PIN_EINK_EN=34 + -D EINK_DISPLAY_MODEL=GxEPD2_213_BN + -D EINK_WIDTH=250 + -D EINK_HEIGHT=122 + -D RADIOLIB_EXCLUDE_SX128X=1 + -D RADIOLIB_EXCLUDE_SX127X=1 + -D RADIOLIB_EXCLUDE_LR11X0=1 +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak4631_epaper_onrxtx> +lib_deps = + ${nrf52840_base.lib_deps} + zinggjm/GxEPD2@^1.5.1 + melopero/Melopero RV3028@^1.1.0 + rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 + beegee-tokyo/RAKwireless RAK12034@^1.0.0 +debug_tool = jlink +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +;upload_protocol = jlink +;upload_port = /dev/ttyACM3 \ No newline at end of file diff --git a/variants/rak4631_epaper_onrxtx/variant.cpp b/variants/rak4631_epaper_onrxtx/variant.cpp new file mode 100644 index 0000000..e84b60b --- /dev/null +++ b/variants/rak4631_epaper_onrxtx/variant.cpp @@ -0,0 +1,45 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 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, + + // 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); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); +} diff --git a/variants/rak4631_epaper_onrxtx/variant.h b/variants/rak4631_epaper_onrxtx/variant.h new file mode 100644 index 0000000..5888cff --- /dev/null +++ b/variants/rak4631_epaper_onrxtx/variant.h @@ -0,0 +1,205 @@ +#ifndef _VARIANT_RAK4630_ +#define _VARIANT_RAK4630_ + +#define RAK4630 + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (35) +#define PIN_LED2 (36) + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 + +#define LED_STATE_ON 1 // State when LED is litted + +/* + * Buttons + */ + +#define PIN_BUTTON1 9 // Pin for button on E-ink button module or IO expansion +#define BUTTON_NEED_PULLUP +// #define PIN_BUTTON2 12 + +/* + * Analog pins + */ +#define PIN_A0 (-1) //(5) +#define PIN_A1 (31) +#define PIN_A2 (28) +#define PIN_A3 (29) +#define PIN_A4 (30) +#define PIN_A5 (31) +#define PIN_A6 (0xff) +#define PIN_A7 (0xff) + +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; +static const uint8_t A3 = PIN_A3; +static const uint8_t A4 = PIN_A4; +static const uint8_t A5 = PIN_A5; +static const uint8_t A6 = PIN_A6; +static const uint8_t A7 = PIN_A7; +#define ADC_RESOLUTION 14 + +// Other pins +#define PIN_AREF (2) +// #define PIN_NFC1 (9) +// #define PIN_NFC2 (10) + +static const uint8_t AREF = PIN_AREF; + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (-1) +#define PIN_SERIAL1_TX (-1) + +// Connected to Jlink CDC +#define PIN_SERIAL2_RX (-1) +#define PIN_SERIAL2_TX (-1) + +// Testing USB detection +#define NRF_APM + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +#define PIN_SPI_MISO (45) +#define PIN_SPI_MOSI (44) +#define PIN_SPI_SCK (43) + +#define PIN_SPI1_MISO (-1) +#define PIN_SPI1_MOSI (0 + 13) +#define PIN_SPI1_SCK (0 + 14) + +static const uint8_t SS = 42; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +/* + * eink display pins + */ + +#define USE_EINK + +#define PIN_EINK_CS (0 + 16) // TX1 +#define PIN_EINK_BUSY (0 + 15) // RX1 +#define PIN_EINK_DC (0 + 17) // IO1 +// #define PIN_EINK_RES (-1) //first try without RESET then connect it to AIN (AIN0 5 ) +#define PIN_EINK_RES (0 + 5) // 2.13 BN Display needs RESET +#define PIN_EINK_SCLK (0 + 14) // SCL +#define PIN_EINK_MOSI (0 + 13) // SDA + +// RAKRGB +#define HAS_NCP5623 + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (13) +#define PIN_WIRE_SCL (14) + +/* @note RAK5005-O GPIO mapping to RAK4631 GPIO ports + RAK5005-O <-> nRF52840 + IO1 <-> P0.17 (Arduino GPIO number 17) + IO2 <-> P1.02 (Arduino GPIO number 34) + IO3 <-> P0.21 (Arduino GPIO number 21) + IO4 <-> P0.04 (Arduino GPIO number 4) + IO5 <-> P0.09 (Arduino GPIO number 9) + IO6 <-> P0.10 (Arduino GPIO number 10) + IO7 <-> P0.28 (Arduino GPIO number 28) + SW1 <-> P0.01 (Arduino GPIO number 1) + A0 <-> P0.04/AIN2 (Arduino Analog A2 + A1 <-> P0.31/AIN7 (Arduino Analog A7 + SPI_CS <-> P0.26 (Arduino GPIO number 26) + */ + +// RAK4630 LoRa module +#define USE_SX1262 +#define SX126X_CS (42) +#define SX126X_DIO1 (47) +#define SX126X_BUSY (46) +#define SX126X_RESET (38) +// #define SX126X_TXEN (39) +// #define SX126X_RXEN (37) +#define SX126X_POWER_EN (37) +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// enables 3.3V periphery like GPS or IO Module +#define PIN_3V3_EN (34) + +// NO GPS +#undef GPS_RX_PIN +#undef GPS_TX_PIN + +// 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 + +// #define GPS_RX_PIN PIN_SERIAL1_RX +// #define GPS_TX_PIN PIN_SERIAL1_TX + +// RAK12002 RTC Module +#define RV3028_RTC (uint8_t)0b1010010 + +// Battery +// The battery sense is hooked to pin A0 (5) +// #define BATTERY_PIN PIN_A0 +// and has 12 bit resolution +// #define BATTERY_SENSE_RESOLUTION_BITS 12 +// #define BATTERY_SENSE_RESOLUTION 4096.0 +// #undef AREF_VOLTAGE +// #define AREF_VOLTAGE 3.0 +// #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +// #define ADC_MULTIPLIER 1.73 + +// #define HAS_RTC 1 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif \ No newline at end of file diff --git a/variants/rak4631_eth_gw/platformio.ini b/variants/rak4631_eth_gw/platformio.ini new file mode 100644 index 0000000..62b7e73 --- /dev/null +++ b/variants/rak4631_eth_gw/platformio.ini @@ -0,0 +1,66 @@ +; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921 +[env:rak4631_eth_gw] +extends = nrf52840_base +board = wiscore_rak4631 +board_check = true +build_flags = ${nrf52840_base.build_flags} -Ivariants/rak4631_eth_gw -D RAK_4631 + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + -DEINK_DISPLAY_MODEL=GxEPD2_213_BN + -DEINK_WIDTH=250 + -DEINK_HEIGHT=122 + -DNRF52_USE_JSON=1 + -DMESHTASTIC_EXCLUDE_GPS=1 + -DMESHTASTIC_EXCLUDE_WIFI=1 + -DMESHTASTIC_EXCLUDE_SCREEN=1 +; -DMESHTASTIC_EXCLUDE_PKI=1 + -DMESHTASTIC_EXCLUDE_POWER_FSM=1 + -DMESHTASTIC_EXCLUDE_POWERMON=1 +; -DMESHTASTIC_EXCLUDE_TZ=1 + -DMESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION=1 + -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 + -DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1 + -DMESHTASTIC_EXCLUDE_STOREFORWARD=1 + -DMESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 + -DMESHTASTIC_EXCLUDE_WAYPOINT=1 +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak4631_eth_gw> + + + +lib_deps = + ${nrf52840_base.lib_deps} + ${networking_base.lib_deps} + melopero/Melopero RV3028@^1.1.0 + https://github.com/RAKWireless/RAK13800-W5100S.git#1.0.2 + rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 + https://github.com/meshtastic/RAK12034-BMX160.git#4821355fb10390ba8557dc43ca29a023bcfbb9d9 + bblanchon/ArduinoJson @ 6.21.4 +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +; Note: as of 6/2013 the serial/bootloader based programming takes approximately 30 seconds +;upload_protocol = jlink + +; Allows programming and debug via the RAK NanoDAP as the default debugger tool for the RAK4631 (it is only $10!) +; programming time is about the same as the bootloader version. +; For information on this see the meshtastic developers documentation for "Development on the NRF52" +[env:rak4631_eth_gw_dbg] +extends = env:rak4631 +board_level = extra + +; if the builtin version of openocd has a buggy version of semihosting, so use the external version +; platform_packages = platformio/tool-openocd@^3.1200.0 + +build_flags = + ${env:rak4631_eth_gw.build_flags} + -D USE_SEMIHOSTING + +lib_deps = + ${env:rak4631_eth_gw.lib_deps} + https://github.com/geeksville/Armduino-Semihosting.git#35b538fdf208c3530c1434cd099a08e486672ee4 + +; NOTE: the pyocd support for semihosting is buggy. So I switched to using the builtin platformio support for the stlink adapter which worked much better. +; However the built in openocd version in platformio has buggy support for TCP to semihosting. +; +; So I'm now trying the external openocd - but the openocd scripts for nrf52.cfg assume you are using a DAP adapter not an STLINK adapter. +; In theory I could change those scripts. But for now I'm trying going back to a DAP adapter but with the external openocd. + +upload_protocol = stlink +; eventually use platformio/tool-pyocd@^2.3600.0 instad +;upload_protocol = custom +;upload_command = pyocd flash -t nrf52840 $UPLOADERFLAGS $SOURCE \ No newline at end of file diff --git a/variants/rak4631_eth_gw/variant.cpp b/variants/rak4631_eth_gw/variant.cpp new file mode 100644 index 0000000..e84b60b --- /dev/null +++ b/variants/rak4631_eth_gw/variant.cpp @@ -0,0 +1,45 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 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, + + // 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); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); +} diff --git a/variants/rak4631_eth_gw/variant.h b/variants/rak4631_eth_gw/variant.h new file mode 100644 index 0000000..bc55413 --- /dev/null +++ b/variants/rak4631_eth_gw/variant.h @@ -0,0 +1,273 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_RAK4630_ +#define _VARIANT_RAK4630_ + +#define RAK4630 + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (35) +#define PIN_LED2 (36) + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 + +#define LED_STATE_ON 1 // State when LED is litted + +/* + * Buttons + */ + +#define PIN_BUTTON1 9 // Pin for button on E-ink button module or IO expansion +#define BUTTON_NEED_PULLUP +#define PIN_BUTTON2 12 +#define PIN_BUTTON3 24 +#define PIN_BUTTON4 25 + +/* + * Analog pins + */ +#define PIN_A0 (5) +#define PIN_A1 (31) +#define PIN_A2 (28) +#define PIN_A3 (29) +#define PIN_A4 (30) +#define PIN_A5 (31) +#define PIN_A6 (0xff) +#define PIN_A7 (0xff) + +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; +static const uint8_t A3 = PIN_A3; +static const uint8_t A4 = PIN_A4; +static const uint8_t A5 = PIN_A5; +static const uint8_t A6 = PIN_A6; +static const uint8_t A7 = PIN_A7; +#define ADC_RESOLUTION 14 + +// Other pins +#define PIN_AREF (2) +#define PIN_NFC1 (9) +#define PIN_NFC2 (10) + +static const uint8_t AREF = PIN_AREF; + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (15) +#define PIN_SERIAL1_TX (16) + +// Connected to Jlink CDC +#define PIN_SERIAL2_RX (8) +#define PIN_SERIAL2_TX (6) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +#define PIN_SPI_MISO (45) +#define PIN_SPI_MOSI (44) +#define PIN_SPI_SCK (43) + +#define PIN_SPI1_MISO (29) // (0 + 29) +#define PIN_SPI1_MOSI (30) // (0 + 30) +#define PIN_SPI1_SCK (3) // (0 + 3) + +static const uint8_t SS = 42; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +/* + * eink display pins + */ + +#define PIN_EINK_CS (0 + 26) +#define PIN_EINK_BUSY (0 + 4) +#define PIN_EINK_DC (0 + 17) +#define PIN_EINK_RES (-1) +#define PIN_EINK_SCLK (0 + 3) +#define PIN_EINK_MOSI (0 + 30) // also called SDI + +// #define USE_EINK + +// RAKRGB +#define HAS_NCP5623 + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (13) +#define PIN_WIRE_SCL (14) + +// QSPI Pins +#define PIN_QSPI_SCK 3 +#define PIN_QSPI_CS 26 +#define PIN_QSPI_IO0 30 +#define PIN_QSPI_IO1 29 +#define PIN_QSPI_IO2 28 +#define PIN_QSPI_IO3 2 + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES IS25LP080D +#define EXTERNAL_FLASH_USE_QSPI + +/* @note RAK5005-O GPIO mapping to RAK4631 GPIO ports + RAK5005-O <-> nRF52840 + IO1 <-> P0.17 (Arduino GPIO number 17) + IO2 <-> P1.02 (Arduino GPIO number 34) + IO3 <-> P0.21 (Arduino GPIO number 21) + IO4 <-> P0.04 (Arduino GPIO number 4) + IO5 <-> P0.09 (Arduino GPIO number 9) + IO6 <-> P0.10 (Arduino GPIO number 10) + IO7 <-> P0.28 (Arduino GPIO number 28) + SW1 <-> P0.01 (Arduino GPIO number 1) + A0 <-> P0.04/AIN2 (Arduino Analog A2 + A1 <-> P0.31/AIN7 (Arduino Analog A7 + SPI_CS <-> P0.26 (Arduino GPIO number 26) + */ + +// RAK4630 LoRa module + +/* Setup of the SX1262 LoRa module ( https://docs.rakwireless.com/Product-Categories/WisBlock/RAK4631/Datasheet/ ) + +P1.10 NSS SPI NSS (Arduino GPIO number 42) +P1.11 SCK SPI CLK (Arduino GPIO number 43) +P1.12 MOSI SPI MOSI (Arduino GPIO number 44) +P1.13 MISO SPI MISO (Arduino GPIO number 45) +P1.14 BUSY BUSY signal (Arduino GPIO number 46) +P1.15 DIO1 DIO1 event interrupt (Arduino GPIO number 47) +P1.06 NRESET NRESET manual reset of the SX1262 (Arduino GPIO number 38) + +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 + +SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG + +*/ + +#define DETECTION_SENSOR_EN 4 + +#define USE_SX1262 +#define SX126X_CS (42) +#define SX126X_DIO1 (47) +#define SX126X_BUSY (46) +#define SX126X_RESET (38) +// #define SX126X_TXEN (39) +// #define SX126X_RXEN (37) +#define SX126X_POWER_EN (37) +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// Testing USB detection +#define NRF_APM + +// enables 3.3V periphery like GPS or IO Module +// Do not toggle this for GPS power savings +#define PIN_3V3_EN (34) + +// 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 + +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +// Define pin to enable GPS toggle (set GPIO to LOW) via user button triple press + +// RAK12002 RTC Module +#define RV3028_RTC (uint8_t)0b1010010 + +// RAK18001 Buzzer in Slot C +// #define PIN_BUZZER 21 // IO3 is PWM2 +// NEW: set this via protobuf instead! + +// Battery +// The battery sense is hooked to pin A0 (5) +#define BATTERY_PIN PIN_A0 +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER 1.73 + +#define HAS_RTC 1 + +#define HAS_ETHERNET 1 + +#define RAK_4631 1 + +#define PIN_ETHERNET_RESET 21 +#define PIN_ETHERNET_SS PIN_EINK_CS +#define ETH_SPI_PORT SPI1 +#define AQ_SET_PIN 10 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif \ No newline at end of file diff --git a/variants/rp2040-lora/platformio.ini b/variants/rp2040-lora/platformio.ini new file mode 100644 index 0000000..8499f6f --- /dev/null +++ b/variants/rp2040-lora/platformio.ini @@ -0,0 +1,16 @@ +[env:rp2040-lora] +extends = rp2040_base +board = rpipico +upload_protocol = picotool + +# add our variants files to the include and src paths +build_flags = ${rp2040_base.build_flags} + -DRP2040_LORA + -Ivariants/rp2040-lora + -DDEBUG_RP2040_PORT=Serial + -DHW_SPI1_DEVICE + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m0plus" +lib_deps = + ${rp2040_base.lib_deps} +debug_build_flags = ${rp2040_base.build_flags} +debug_tool = cmsis-dap ; for e.g. Picotool \ No newline at end of file diff --git a/variants/rp2040-lora/variant.h b/variants/rp2040-lora/variant.h new file mode 100644 index 0000000..f182660 --- /dev/null +++ b/variants/rp2040-lora/variant.h @@ -0,0 +1,60 @@ +// #define RADIOLIB_CUSTOM_ARDUINO 1 +// #define RADIOLIB_TONE_UNSUPPORTED 1 +// #define RADIOLIB_SOFTWARE_SERIAL_UNSUPPORTED 1 + +#define ARDUINO_ARCH_AVR + +// #define USE_SH1106 1 + +// default I2C pins: +// SDA = 4 +// SCL = 5 + +// Recommended pins for SerialModule: +// txd = 8 +// rxd = 9 + +#define EXT_NOTIFY_OUT 22 +#undef BUTTON_PIN // Pin 17 used for antenna switching via DIO4 + +#define LED_PIN PIN_LED + +// #define BATTERY_PIN 26 +// ratio of voltage divider = 3.0 (R17=200k, R18=100k) +// #define ADC_MULTIPLIER 3.1 // 3.0 + a bit for being optimistic + +#define HAS_CPU_SHUTDOWN 1 +#define USE_SX1262 + +#undef LORA_SCK +#undef LORA_MISO +#undef LORA_MOSI +#undef LORA_CS + +// https://www.waveshare.com/rp2040-lora.htm +// https://www.waveshare.com/img/devkit/RP2040-LoRa-HF/RP2040-LoRa-HF-details-11.jpg +#define LORA_SCK 14 // GPIO14 +#define LORA_MISO 24 // GPIO24 +#define LORA_MOSI 15 // GPIO15 +#define LORA_CS 13 // GPIO13 + +#define LORA_DIO0 RADIOLIB_NC // No GPIO connection +#define LORA_RESET 23 // GPIO23 +#define LORA_BUSY 18 // GPIO18 +#define LORA_DIO1 16 // GPIO16 +#define LORA_DIO2 RADIOLIB_NC // Antenna switching, no GPIO connection +#define LORA_DIO3 RADIOLIB_NC // No GPIO connection +#define LORA_DIO4 17 // GPIO17 + +// On rp2040-lora board the antenna switch is wired and works with complementary-pin control logic. +// See PE4259 datasheet page 4 + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_BUSY +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH // Antenna switch CTRL +#define SX126X_RXEN LORA_DIO4 // Antenna switch !CTRL via GPIO17 +// #define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif \ No newline at end of file diff --git a/variants/rpipico-slowclock/platformio.ini b/variants/rpipico-slowclock/platformio.ini new file mode 100644 index 0000000..c219942 --- /dev/null +++ b/variants/rpipico-slowclock/platformio.ini @@ -0,0 +1,29 @@ +[env:pico_slowclock] +extends = rp2040_base +board = rpipico +board_level = extra +upload_protocol = jlink +# debug settings for external openocd with RP2040 support (custom build) +debug_tool = custom +debug_init_cmds = + target extended-remote localhost:3333 + $INIT_BREAK + monitor reset halt + $LOAD_CMDS + monitor init + monitor reset halt + +# add our variants files to the include and src paths +build_flags = ${rp2040_base.build_flags} + -DRPI_PICO + -Ivariants/rpipico-slowclock + -DDEBUG_RP2040_PORT=Serial2 + -DHW_SPI1_DEVICE + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m0plus" + -g + -DNO_USB +lib_deps = + ${rp2040_base.lib_deps} +debug_build_flags = ${rp2040_base.build_flags} + -g + -DNO_USB \ No newline at end of file diff --git a/variants/rpipico-slowclock/variant.h b/variants/rpipico-slowclock/variant.h new file mode 100644 index 0000000..fb97ec0 --- /dev/null +++ b/variants/rpipico-slowclock/variant.h @@ -0,0 +1,87 @@ +#define ARDUINO_ARCH_AVR + +// Build with slow system clock enabled to reduce power consumption. +#define RP2040_SLOW_CLOCK + +#ifdef RP2040_SLOW_CLOCK +// Redefine UART1 serial log output to avoid collision with UART0 for GPS. +#define SERIAL2_TX 4 +#define SERIAL2_RX 5 +// Reroute log output in SensorLib when USB is not available +#define log_e(...) Serial2.printf(__VA_ARGS__) +#define log_i(...) Serial2.printf(__VA_ARGS__) +#define log_d(...) Serial2.printf(__VA_ARGS__) +#endif + +// Expecting the Waveshare Pico GPS hat +#define HAS_GPS 1 + +// Enable OLED Screen +#define HAS_SCREEN 1 +#define USE_SH1106 1 +#define RESET_OLED 13 + +// Redefine I2C0 pins to avoid collision with UART1/Serial2. +#define I2C_SDA 8 +#define I2C_SCL 9 + +// Redefine Waveshare UPS-A/B I2C_1 pins: +#define I2C_SDA1 6 +#define I2C_SCL1 7 +// Waveshare UPS-A/B uses a 0.01 Ohm shunt for the INA219 sensor +#define INA219_MULTIPLIER 10.0f + +// Waveshare Pico GPS L76B pins: +#define GPS_RX_PIN 1 +#define GPS_TX_PIN 0 + +// Wakeup from backup mode +// #define PIN_GPS_FORCE_ON 14 +// No GPS reset available +#undef PIN_GPS_RESET +/* + * For PPS output the resistor R20 needs to be populated with 0 Ohm + * on the Waveshare Pico GPS board. + */ +#define PIN_GPS_PPS 16 +/* + * For standby mode switching the resistor R18 needs to be populated + * with 0 Ohm on the Waveshare Pico GPS board. + */ +#define PIN_GPS_STANDBY 17 + +#define BUTTON_PIN 18 +#define EXT_NOTIFY_OUT 22 +#define LED_PIN PIN_LED + +#define BATTERY_PIN 26 +// ratio of voltage divider = 3.0 (R17=200k, R18=100k) +#define ADC_MULTIPLIER 3.1 // 3.0 + a bit for being optimistic +#define BATTERY_SENSE_RESOLUTION_BITS ADC_RESOLUTION + +#define USE_SX1262 + +#undef LORA_SCK +#undef LORA_MISO +#undef LORA_MOSI +#undef LORA_CS + +#define LORA_SCK 10 +#define LORA_MISO 12 +#define LORA_MOSI 11 +#define LORA_CS 3 + +#define LORA_DIO0 RADIOLIB_NC +#define LORA_RESET 15 +#define LORA_DIO1 20 +#define LORA_DIO2 2 +#define LORA_DIO3 RADIOLIB_NC + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif \ No newline at end of file diff --git a/variants/rpipico/platformio.ini b/variants/rpipico/platformio.ini new file mode 100644 index 0000000..e4b9e47 --- /dev/null +++ b/variants/rpipico/platformio.ini @@ -0,0 +1,16 @@ +[env:pico] +extends = rp2040_base +board = rpipico +upload_protocol = picotool + +# add our variants files to the include and src paths +build_flags = ${rp2040_base.build_flags} + -DRPI_PICO + -Ivariants/rpipico + -DDEBUG_RP2040_PORT=Serial + -DHW_SPI1_DEVICE + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m0plus" +lib_deps = + ${rp2040_base.lib_deps} +debug_build_flags = ${rp2040_base.build_flags} +debug_tool = cmsis-dap ; for e.g. Picotool \ No newline at end of file diff --git a/variants/rpipico/variant.h b/variants/rpipico/variant.h new file mode 100644 index 0000000..7efaeaf --- /dev/null +++ b/variants/rpipico/variant.h @@ -0,0 +1,50 @@ +// #define RADIOLIB_CUSTOM_ARDUINO 1 +// #define RADIOLIB_TONE_UNSUPPORTED 1 +// #define RADIOLIB_SOFTWARE_SERIAL_UNSUPPORTED 1 + +#define ARDUINO_ARCH_AVR + +// default I2C pins: +// SDA = 4 +// SCL = 5 + +// Recommended pins for SerialModule: +// txd = 8 +// rxd = 9 + +#define EXT_NOTIFY_OUT 22 +#define BUTTON_PIN 17 + +#define LED_PIN PIN_LED + +#define BATTERY_PIN 26 +// ratio of voltage divider = 3.0 (R17=200k, R18=100k) +#define ADC_MULTIPLIER 3.1 // 3.0 + a bit for being optimistic +#define BATTERY_SENSE_RESOLUTION_BITS ADC_RESOLUTION + +#define USE_SX1262 + +#undef LORA_SCK +#undef LORA_MISO +#undef LORA_MOSI +#undef LORA_CS + +#define LORA_SCK 10 +#define LORA_MISO 12 +#define LORA_MOSI 11 +#define LORA_CS 3 + +#define LORA_DIO0 RADIOLIB_NC +#define LORA_RESET 15 +#define LORA_DIO1 20 +#define LORA_DIO2 2 +#define LORA_DIO3 RADIOLIB_NC + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif diff --git a/variants/rpipico2/platformio.ini b/variants/rpipico2/platformio.ini new file mode 100644 index 0000000..a634144 --- /dev/null +++ b/variants/rpipico2/platformio.ini @@ -0,0 +1,16 @@ +[env:pico2] +extends = rp2350_base +board = rpipico2 +upload_protocol = picotool + +# add our variants files to the include and src paths +build_flags = ${rp2350_base.build_flags} + -DRPI_PICO2 + -Ivariants/rpipico2 + -DDEBUG_RP2040_PORT=Serial + -DHW_SPI1_DEVICE + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m0plus" +lib_deps = + ${rp2350_base.lib_deps} +debug_build_flags = ${rp2350_base.build_flags} +debug_tool = cmsis-dap ; for e.g. Picotool \ No newline at end of file diff --git a/variants/rpipico2/variant.h b/variants/rpipico2/variant.h new file mode 100644 index 0000000..7efaeaf --- /dev/null +++ b/variants/rpipico2/variant.h @@ -0,0 +1,50 @@ +// #define RADIOLIB_CUSTOM_ARDUINO 1 +// #define RADIOLIB_TONE_UNSUPPORTED 1 +// #define RADIOLIB_SOFTWARE_SERIAL_UNSUPPORTED 1 + +#define ARDUINO_ARCH_AVR + +// default I2C pins: +// SDA = 4 +// SCL = 5 + +// Recommended pins for SerialModule: +// txd = 8 +// rxd = 9 + +#define EXT_NOTIFY_OUT 22 +#define BUTTON_PIN 17 + +#define LED_PIN PIN_LED + +#define BATTERY_PIN 26 +// ratio of voltage divider = 3.0 (R17=200k, R18=100k) +#define ADC_MULTIPLIER 3.1 // 3.0 + a bit for being optimistic +#define BATTERY_SENSE_RESOLUTION_BITS ADC_RESOLUTION + +#define USE_SX1262 + +#undef LORA_SCK +#undef LORA_MISO +#undef LORA_MOSI +#undef LORA_CS + +#define LORA_SCK 10 +#define LORA_MISO 12 +#define LORA_MOSI 11 +#define LORA_CS 3 + +#define LORA_DIO0 RADIOLIB_NC +#define LORA_RESET 15 +#define LORA_DIO1 20 +#define LORA_DIO2 2 +#define LORA_DIO3 RADIOLIB_NC + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif diff --git a/variants/rpipicow/platformio.ini b/variants/rpipicow/platformio.ini new file mode 100644 index 0000000..2600b4b --- /dev/null +++ b/variants/rpipicow/platformio.ini @@ -0,0 +1,18 @@ +[env:picow] +extends = rp2040_base +board = rpipicow +upload_protocol = picotool + +# add our variants files to the include and src paths +build_flags = ${rp2040_base.build_flags} + -DRPI_PICO + -Ivariants/rpipicow + -DHW_SPI1_DEVICE + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m0plus" + -fexceptions # for exception handling in MQTT +build_src_filter = ${rp2040_base.build_src_filter} + +lib_deps = + ${rp2040_base.lib_deps} + ${networking_base.lib_deps} +debug_build_flags = ${rp2040_base.build_flags} +debug_tool = cmsis-dap ; for e.g. Picotool \ No newline at end of file diff --git a/variants/rpipicow/variant.h b/variants/rpipicow/variant.h new file mode 100644 index 0000000..24da8f9 --- /dev/null +++ b/variants/rpipicow/variant.h @@ -0,0 +1,54 @@ +// #define RADIOLIB_CUSTOM_ARDUINO 1 +// #define RADIOLIB_TONE_UNSUPPORTED 1 +// #define RADIOLIB_SOFTWARE_SERIAL_UNSUPPORTED 1 + +#define ARDUINO_ARCH_AVR + +#ifndef HAS_WIFI +#define HAS_WIFI 1 +#endif + +// default I2C pins: +// SDA = 4 +// SCL = 5 + +// Recommended pins for SerialModule: +// txd = 8 +// rxd = 9 + +#define EXT_NOTIFY_OUT 22 +#define BUTTON_PIN 17 + +#define LED_PIN LED_BUILTIN + +#define BATTERY_PIN 26 +// ratio of voltage divider = 3.0 (R17=200k, R18=100k) +#define ADC_MULTIPLIER 3.1 // 3.0 + a bit for being optimistic +#define BATTERY_SENSE_RESOLUTION_BITS ADC_RESOLUTION + +#define USE_SX1262 + +#undef LORA_SCK +#undef LORA_MISO +#undef LORA_MOSI +#undef LORA_CS + +#define LORA_SCK 10 +#define LORA_MISO 12 +#define LORA_MOSI 11 +#define LORA_CS 3 + +#define LORA_DIO0 RADIOLIB_NC +#define LORA_RESET 15 +#define LORA_DIO1 20 +#define LORA_DIO2 2 +#define LORA_DIO3 RADIOLIB_NC + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif diff --git a/variants/seeed-sensecap-indicator/pins_arduino.h b/variants/seeed-sensecap-indicator/pins_arduino.h new file mode 100644 index 0000000..300f0e0 --- /dev/null +++ b/variants/seeed-sensecap-indicator/pins_arduino.h @@ -0,0 +1,56 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +// static const uint8_t LED_BUILTIN = -1; + +// static const uint8_t TX = 43; +// static const uint8_t RX = 44; + +static const uint8_t SDA = 39; +static const uint8_t SCL = 40; + +// Default SPI will be mapped to Radio +static const uint8_t SS = -1; +static const uint8_t MOSI = 48; +static const uint8_t MISO = 47; +static const uint8_t SCK = 41; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +#endif /* Pins_Arduino_h */ \ No newline at end of file diff --git a/variants/seeed-sensecap-indicator/platformio.ini b/variants/seeed-sensecap-indicator/platformio.ini new file mode 100644 index 0000000..e6bb214 --- /dev/null +++ b/variants/seeed-sensecap-indicator/platformio.ini @@ -0,0 +1,28 @@ +; Seeed Studio SenseCAP Indicator +[env:seeed-sensecap-indicator] +extends = esp32s3_base +platform_packages = + platformio/framework-arduinoespressif32 @ https://github.com/mverch67/arduino-esp32.git#add_tca9535 ; based on 2.0.16 + +board = seeed-sensecap-indicator +board_check = true +upload_protocol = esptool + +build_flags = ${esp32_base.build_flags} + -Ivariants/seeed-sensecap-indicator + -DSENSECAP_INDICATOR + -DCONFIG_ARDUHAL_LOG_COLORS + -DRADIOLIB_DEBUG_SPI=0 + -DRADIOLIB_DEBUG_PROTOCOL=0 + -DRADIOLIB_DEBUG_BASIC=0 + -DRADIOLIB_VERBOSE_ASSERT=0 + -DRADIOLIB_SPI_PARANOID=0 + -DIO_EXPANDER=0x40 + -DIO_EXPANDER_IRQ=42 + ;-DIO_EXPANDER_DEBUG + -DUSE_ARDUINO_HAL_GPIO + +lib_deps = ${esp32s3_base.lib_deps} + https://github.com/mverch67/LovyanGFX#develop + earlephilhower/ESP8266Audio@^1.9.7 + earlephilhower/ESP8266SAM@^1.0.1 \ No newline at end of file diff --git a/variants/seeed-sensecap-indicator/variant.h b/variants/seeed-sensecap-indicator/variant.h new file mode 100644 index 0000000..d7ed329 --- /dev/null +++ b/variants/seeed-sensecap-indicator/variant.h @@ -0,0 +1,64 @@ +#define I2C_SDA 39 +#define I2C_SCL 40 + +#define BUTTON_PIN 38 +// #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 + +// ST7701 TFT LCD +#define ST7701_CS (4 | IO_EXPANDER) +#define ST7701_RS -1 // DC +#define ST7701_SDA 48 // MOSI +#define ST7701_SCK 41 +#define ST7701_RESET (5 | IO_EXPANDER) +#define ST7701_MISO 47 +#define ST7701_BUSY -1 +#define ST7701_BL 45 +#define ST7701_SPI_HOST SPI2_HOST +#define ST7701_BACKLIGHT_EN 45 +#define SPI_FREQUENCY 20000000 +#define SPI_READ_FREQUENCY 16000000 +#define TFT_HEIGHT 480 +#define TFT_WIDTH 480 +#define TFT_OFFSET_X 0 +#define TFT_OFFSET_Y 0 +#define TFT_OFFSET_ROTATION 0 +#define TFT_BL 45 +#define SCREEN_ROTATE +#define SCREEN_TRANSITION_FRAMERATE 5 // fps + +#define HAS_TOUCHSCREEN 1 +#define SCREEN_TOUCH_INT (6 | IO_EXPANDER) +#define SCREEN_TOUCH_RST (7 | IO_EXPANDER) +#define TOUCH_I2C_PORT 0 +#define TOUCH_SLAVE_ADDRESS 0x48 + +// Buzzer +#define PIN_BUZZER 19 + +#define HAS_GPS 0 +#undef GPS_RX_PIN +#undef GPS_TX_PIN + +#define USE_SX1262 +#define USE_SX1268 + +#define LORA_SCK 41 +#define LORA_MISO 47 +#define LORA_MOSI 48 +#define LORA_CS (0 | IO_EXPANDER) + +#define LORA_DIO0 -1 // a no connect on the SX1262 module +#define LORA_RESET (1 | IO_EXPANDER) +#define LORA_DIO1 (3 | IO_EXPANDER) // SX1262 IRQ +#define LORA_DIO2 (2 | IO_EXPANDER) // SX1262 BUSY +#define LORA_DIO3 + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH diff --git a/variants/seeed_xiao_s3/pins_arduino.h b/variants/seeed_xiao_s3/pins_arduino.h new file mode 100644 index 0000000..52e96ea --- /dev/null +++ b/variants/seeed_xiao_s3/pins_arduino.h @@ -0,0 +1,21 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x2886 +#define USB_PID 0x0059 + +// GPIO48 Reference: https://github.com/espressif/arduino-esp32/pull/8600 + +// The default Wire will be mapped to Screen and Sensors +static const uint8_t SDA = 47; +static const uint8_t SCL = 48; + +// Default SPI will be mapped to Radio +static const uint8_t MISO = 8; +static const uint8_t SCK = 7; +static const uint8_t MOSI = 9; +static const uint8_t SS = 41; + +#endif /* Pins_Arduino_h */ diff --git a/variants/seeed_xiao_s3/platformio.ini b/variants/seeed_xiao_s3/platformio.ini new file mode 100644 index 0000000..3d10d71 --- /dev/null +++ b/variants/seeed_xiao_s3/platformio.ini @@ -0,0 +1,17 @@ +[env:seeed-xiao-s3] +extends = esp32s3_base +board = seeed-xiao-s3 +board_check = true +board_build.mcu = esp32s3 +upload_protocol = esptool +upload_speed = 921600 +lib_deps = + ${esp32s3_base.lib_deps} +build_unflags = + ${esp32s3_base.build_unflags} + -DARDUINO_USB_MODE=1 +build_flags = + ${esp32s3_base.build_flags} -DSEEED_XIAO_S3 -I variants/seeed_xiao_s3 + -DBOARD_HAS_PSRAM + + -DARDUINO_USB_MODE=0 \ No newline at end of file diff --git a/variants/seeed_xiao_s3/variant.h b/variants/seeed_xiao_s3/variant.h new file mode 100644 index 0000000..ab886d3 --- /dev/null +++ b/variants/seeed_xiao_s3/variant.h @@ -0,0 +1,84 @@ +/* + ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄ +▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░▌ +▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀█░▌ +▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ +▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌ +▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░▌ ▐░▌ + ▀▀▀▀▀▀▀▀▀█░▌▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░▌ ▐░▌ + ▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ + ▄▄▄▄▄▄▄▄▄█░▌▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄█░▌ +▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░▌ + ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ + + ▄ ▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ + ▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ + ▐░▌ ▐░▌ ▀▀▀▀█░█▀▀▀▀ ▐░█▀▀▀▀▀▀▀█░▌▐░█▀▀▀▀▀▀▀█░▌ ▐░█▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀█░▌ + ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ + ▐░▐░▌ ▐░▌ ▐░█▄▄▄▄▄▄▄█░▌▐░▌ ▐░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄█░▌ + ▐░▌ ▐░▌ ▐░░░░░░░░░░░▌▐░▌ ▐░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ + ▐░▌░▌ ▐░▌ ▐░█▀▀▀▀▀▀▀█░▌▐░▌ ▐░▌ ▀▀▀▀▀▀▀▀▀█░▌ ▀▀▀▀▀▀▀▀▀█░▌ + ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ + ▐░▌ ▐░▌ ▄▄▄▄█░█▄▄▄▄ ▐░▌ ▐░▌▐░█▄▄▄▄▄▄▄█░▌ ▄▄▄▄▄▄▄▄▄█░▌ ▄▄▄▄▄▄▄▄▄█░▌ + ▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░▌ ▐░▌▐░░░░░░░░░░░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ + ▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ +*/ + +/* +Board Information: https://www.seeedstudio.com/XIAO-ESP32S3-Sense-p-5639.html +Expansion Board Infomation : https://www.seeedstudio.com/Seeeduino-XIAO-Expansion-board-p-4746.html +L76K GPS Module Information : https://www.seeedstudio.com/L76K-GNSS-Module-for-Seeed-Studio-XIAO-p-5864.html +*/ + +#define LED_PIN 48 +#define LED_STATE_ON 1 // State when LED is lit + +#define BUTTON_PIN 21 // This is the Program Button +#define BUTTON_NEED_PULLUP + +/*Warning: + https://www.seeedstudio.com/L76K-GNSS-Module-for-Seeed-Studio-XIAO-p-5864.html + L76K Expansion Board can not directly used, L76K Reset Pin needs to override or physically remove it, + otherwise it will conflict with the SPI pins +*/ +// #define GPS_L76K +#ifdef GPS_L76K +#define GPS_RX_PIN 44 +#define GPS_TX_PIN 43 +#define HAS_GPS 1 +#define GPS_BAUDRATE 9600 +#define GPS_THREAD_INTERVAL 50 +#define PIN_SERIAL1_RX PIN_GPS_TX +#define PIN_SERIAL1_TX PIN_GPS_RX +#define PIN_GPS_STANDBY 1 +#endif + +// XIAO S3 Expansion board has 1.3 inch OLED Screen +#define USCREEN_SSD1306 + +#define I2C_SDA 5 +#define I2C_SCL 6 + +// XIAO S3 LORA module +#define USE_SX1262 + +#define LORA_MISO 8 +#define LORA_SCK 7 +#define LORA_MOSI 9 +#define LORA_CS 41 + +#define LORA_RESET 42 +#define LORA_DIO1 39 + +#define LORA_DIO2 38 + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY 40 +#define SX126X_RESET LORA_RESET + +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif \ No newline at end of file diff --git a/variants/senselora_rp2040/pins_arduino.h b/variants/senselora_rp2040/pins_arduino.h new file mode 100644 index 0000000..bb0ee63 --- /dev/null +++ b/variants/senselora_rp2040/pins_arduino.h @@ -0,0 +1,50 @@ +#pragma once + +#define PIN_A0 (26u) +#define PIN_A1 (27u) +#define PIN_A2 (28u) +#define PIN_A3 (29u) + +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; +static const uint8_t A3 = PIN_A3; + +// LEDs +#define PIN_LED (23u) +#define PIN_LED1 PIN_LED +#define LED_BUILTIN PIN_LED + +#define ADC_RESOLUTION 12 + +// Serial +#define PIN_SERIAL1_TX (0ul) +#define PIN_SERIAL1_RX (1ul) + +#define PIN_SERIAL2_TX (4ul) +#define PIN_SERIAL2_RX (5ul) + +// SPI +#define PIN_SPI0_MISO (16u) +#define PIN_SPI0_MOSI (19u) +#define PIN_SPI0_SCK (18u) +#define PIN_SPI0_SS (17u) + +// Wire +#define PIN_WIRE0_SDA (6u) +#define PIN_WIRE0_SCL (7u) + +#define PIN_WIRE1_SDA (-1) +#define PIN_WIRE1_SCL (-1) + +#define SERIAL_HOWMANY (3u) +#define SPI_HOWMANY (2u) +#define WIRE_HOWMANY (1u) + +static const uint8_t SS = PIN_SPI0_SS; +static const uint8_t MOSI = PIN_SPI0_MOSI; +static const uint8_t MISO = PIN_SPI0_MISO; +static const uint8_t SCK = PIN_SPI0_SCK; + +static const uint8_t SDA = PIN_WIRE0_SDA; +static const uint8_t SCL = PIN_WIRE0_SCL; \ No newline at end of file diff --git a/variants/senselora_rp2040/platformio.ini b/variants/senselora_rp2040/platformio.ini new file mode 100644 index 0000000..852ecbb --- /dev/null +++ b/variants/senselora_rp2040/platformio.ini @@ -0,0 +1,14 @@ +[env:senselora_rp2040] +board_level = extra +extends = rp2040_base +board = rpipico +upload_protocol = picotool + +# add our variants files to the include and src paths +build_flags = ${rp2040_base.build_flags} + -DSENSELORA_RP2040 + -Ivariants/senselora_rp2040 + -DDEBUG_RP2040_PORT=Serial + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m0plus" +lib_deps = + ${rp2040_base.lib_deps} \ No newline at end of file diff --git a/variants/senselora_rp2040/variant.h b/variants/senselora_rp2040/variant.h new file mode 100644 index 0000000..2f68cf0 --- /dev/null +++ b/variants/senselora_rp2040/variant.h @@ -0,0 +1,27 @@ +#define ARDUINO_ARCH_AVR + +#define USE_SSD1306 + +#define BUTTON_PIN 2 +#define BUTTON_NEED_PULLUP + +#define LED_PIN PIN_LED + +#undef BATTERY_PIN +#define BATTERY_SENSE_RESOLUTION_BITS ADC_RESOLUTION + +#undef LORA_SCK +#undef LORA_MISO +#undef LORA_MOSI +#undef LORA_CS + +#define USE_RF95 +#define LORA_SCK PIN_SPI0_SCK +#define LORA_MISO PIN_SPI0_MISO +#define LORA_MOSI PIN_SPI0_MOSI +#define LORA_CS PIN_SPI0_SS + +#define LORA_DIO0 21 +#define LORA_DIO1 22 +#define LORA_DIO2 RADIOLIB_NC +#define LORA_RESET 20 \ No newline at end of file diff --git a/variants/station-g1/platformio.ini b/variants/station-g1/platformio.ini new file mode 100644 index 0000000..a466414 --- /dev/null +++ b/variants/station-g1/platformio.ini @@ -0,0 +1,8 @@ +; The 1.0 release of the nano-g1 board +[env:station-g1] +extends = esp32_base +board = ttgo-t-beam +lib_deps = + ${esp32_base.lib_deps} +build_flags = + ${esp32_base.build_flags} -D STATION_G1 -I variants/station-g1 \ No newline at end of file diff --git a/variants/station-g1/variant.h b/variants/station-g1/variant.h new file mode 100644 index 0000000..9a3c37b --- /dev/null +++ b/variants/station-g1/variant.h @@ -0,0 +1,48 @@ +// #define BUTTON_NEED_PULLUP // if set we need to turn on the internal CPU pullup during sleep + +#define I2C_SDA 21 +#define I2C_SCL 22 + +#define I2C_SDA1 14 // Second i2c channel on external IO connector +#define I2C_SCL1 15 // Second i2c channel on external IO connector + +#define BUTTON_PIN 36 // The middle button GPIO on the Nano G1 +// #define BUTTON_PIN_ALT 13 // Alternate GPIO for an external button if needed. Does anyone use this? It is not documented +// anywhere. +#define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module. + +// 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 +#define USE_SX1262 + +#define LORA_DIO0 26 // a No connect on the SX1262 module +#define LORA_RESET 23 +#define LORA_DIO1 33 // SX1262 IRQ +#define LORA_DIO2 32 // SX1262 BUSY +#define LORA_DIO3 // Not connected on PCB + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS // FIXME - we really should define LORA_CS instead +#define SX126X_DIO1 LORA_DIO1 +#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 +#endif + +#define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +#define ADC_CHANNEL ADC1_GPIO35_CHANNEL +#define BATTERY_SENSE_SAMPLES 30 // Set the number of samples, It has an effect of increasing sensitivity. +#define ADC_MULTIPLIER 6.45 +#define CELL_TYPE_LION // same curve for liion/lipo +#define NUM_CELLS 3 + +// different screen +#define USE_SH1106 + +// Station may not have GPS installed, but it has a labeled GPS pinout +#define GPS_RX_PIN 34 +#define GPS_TX_PIN 12 diff --git a/variants/station-g2/pins_arduino.h b/variants/station-g2/pins_arduino.h new file mode 100644 index 0000000..6a80300 --- /dev/null +++ b/variants/station-g2/pins_arduino.h @@ -0,0 +1,21 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// GPIO48 Reference: https://github.com/espressif/arduino-esp32/pull/8600 + +// The default Wire will be mapped to Screen and Sensors +static const uint8_t SDA = 5; +static const uint8_t SCL = 6; + +// Default SPI will be mapped to Radio +static const uint8_t MISO = 14; +static const uint8_t SCK = 12; +static const uint8_t MOSI = 13; +static const uint8_t SS = 11; + +#endif /* Pins_Arduino_h */ \ No newline at end of file diff --git a/variants/station-g2/platformio.ini b/variants/station-g2/platformio.ini new file mode 100644 index 0000000..b674c8b --- /dev/null +++ b/variants/station-g2/platformio.ini @@ -0,0 +1,18 @@ +[env:station-g2] +extends = esp32s3_base +board = station-g2 +board_check = true +board_build.mcu = esp32s3 +upload_protocol = esptool +;upload_port = /dev/ttyACM0 +upload_speed = 921600 +lib_deps = + ${esp32s3_base.lib_deps} +build_unflags = + ${esp32s3_base.build_unflags} + -DARDUINO_USB_MODE=1 +build_flags = + ${esp32s3_base.build_flags} -D STATION_G2 -I variants/station-g2 + -DBOARD_HAS_PSRAM + -DSTATION_G2 + -DARDUINO_USB_MODE=0 \ No newline at end of file diff --git a/variants/station-g2/variant.h b/variants/station-g2/variant.h new file mode 100644 index 0000000..8f0b4b2 --- /dev/null +++ b/variants/station-g2/variant.h @@ -0,0 +1,54 @@ +/* +Board Information: https://wiki.uniteng.com/en/meshtastic/station-g2 +*/ + +// Station G2 may not have GPS installed, but it has a GROVE GPS Socket for Optional GPS Module +#define GPS_RX_PIN 7 +#define GPS_TX_PIN 15 + +// Station G2 has 1.3 inch OLED Screen +#define USE_SH1107_128_64 + +#define I2C_SDA 5 // I2C pins for this board +#define I2C_SCL 6 + +#define BUTTON_PIN 38 // This is the Program Button +#define BUTTON_NEED_PULLUP + +#define USE_SX1262 + +#define LORA_MISO 14 +#define LORA_SCK 12 +#define LORA_MOSI 13 +#define LORA_CS 11 + +#define LORA_RESET 21 +#define LORA_DIO1 48 + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS // FIXME - we really should define LORA_CS instead +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY 47 +#define SX126X_RESET LORA_RESET + +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// Ensure the PA does not exceed the saturation output power. More +// Info:https://wiki.uniteng.com/en/meshtastic/station-g2#summary-for-lora-power-amplifier-conduction-test +#define SX126X_MAX_POWER 19 +#endif + +/* +#define BATTERY_PIN 4 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +#define ADC_CHANNEL ADC1_GPIO4_CHANNEL +#define ADC_MULTIPLIER 4 +#define BATTERY_SENSE_SAMPLES 15 // Set the number of samples, It has an effect of increasing sensitivity. +#define BAT_FULLVOLT 8400 +#define BAT_EMPTYVOLT 5000 +#define BAT_CHARGINGVOLT 8400 +#define BAT_NOBATVOLT 4460 +#define CELL_TYPE_LION // same curve for liion/lipo +#define NUM_CELLS 2 +*/ diff --git a/variants/t-deck/pins_arduino.h b/variants/t-deck/pins_arduino.h new file mode 100644 index 0000000..cb429d7 --- /dev/null +++ b/variants/t-deck/pins_arduino.h @@ -0,0 +1,61 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// static const uint8_t LED_BUILTIN = -1; + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 18; +static const uint8_t SCL = 8; + +// Default SPI will be mapped to Radio +static const uint8_t SS = 9; +static const uint8_t MOSI = 41; +static const uint8_t MISO = 38; +static const uint8_t SCK = 40; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +static const uint8_t BAT_ADC_PIN = 4; + +#endif /* Pins_Arduino_h */ diff --git a/variants/t-deck/platformio.ini b/variants/t-deck/platformio.ini new file mode 100644 index 0000000..a63ff57 --- /dev/null +++ b/variants/t-deck/platformio.ini @@ -0,0 +1,19 @@ +; LilyGo T-Deck +[env:t-deck] +extends = esp32s3_base +board = t-deck +board_check = true +upload_protocol = esptool +#upload_port = COM29 + +build_flags = ${esp32_base.build_flags} + -DT_DECK + -DBOARD_HAS_PSRAM + -DMAX_THREADS=40 + -DGPS_POWER_TOGGLE + -Ivariants/t-deck + +lib_deps = ${esp32s3_base.lib_deps} + lovyan03/LovyanGFX@^1.1.9 + earlephilhower/ESP8266Audio@^1.9.7 + earlephilhower/ESP8266SAM@^1.0.1 \ No newline at end of file diff --git a/variants/t-deck/variant.h b/variants/t-deck/variant.h new file mode 100644 index 0000000..9860d60 --- /dev/null +++ b/variants/t-deck/variant.h @@ -0,0 +1,100 @@ +// ST7789 TFT LCD +#define ST7789_CS 12 +#define ST7789_RS 11 // DC +#define ST7789_SDA 41 // MOSI +#define ST7789_SCK 40 +#define ST7789_RESET -1 +#define ST7789_MISO 38 +#define ST7789_BUSY -1 +#define ST7789_BL 42 +#define ST7789_SPI_HOST SPI2_HOST +#define TFT_BL 42 +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 16000000 +#define TFT_HEIGHT 320 +#define TFT_WIDTH 240 +#define TFT_OFFSET_X 0 +#define TFT_OFFSET_Y 0 +#define TFT_OFFSET_ROTATION 0 +#define SCREEN_ROTATE +#define SCREEN_TRANSITION_FRAMERATE 5 +#define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness + +#define HAS_TOUCHSCREEN 1 +#define SCREEN_TOUCH_INT 16 +#define TOUCH_I2C_PORT 0 +#define TOUCH_SLAVE_ADDRESS 0x5D // GT911 + +#define SLEEP_TIME 120 + +#define BUTTON_PIN 0 +// #define BUTTON_NEED_PULLUP + +#define GPS_RX_PIN 44 +#define GPS_TX_PIN 43 + +// Have SPI interface SD card slot +#define HAS_SDCARD 1 +#define SPI_MOSI (41) +#define SPI_SCK (40) +#define SPI_MISO (38) +#define SPI_CS (39) +#define SDCARD_CS SPI_CS + +#define BATTERY_PIN 4 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +// ratio of voltage divider = 2.0 (RD2=100k, RD3=100k) +#define ADC_MULTIPLIER 2.11 // 2.0 + 10% for correction of display undervoltage. +#define ADC_CHANNEL ADC1_GPIO4_CHANNEL + +// keyboard +#define I2C_SDA 18 // I2C pins for this board +#define I2C_SCL 8 +#define KB_POWERON 10 // must be set to HIGH +#define KB_SLAVE_ADDRESS TDECK_KB_ADDR // 0x55 +#define KB_BL_PIN 46 // not used for now +#define CANNED_MESSAGE_MODULE_ENABLE 1 + +// trackball +#define HAS_TRACKBALL 1 +#define TB_UP 3 +#define TB_DOWN 15 +#define TB_LEFT 1 +#define TB_RIGHT 2 +#define TB_PRESS BUTTON_PIN + +// microphone +#define ES7210_SCK 47 +#define ES7210_DIN 14 +#define ES7210_LRCK 21 +#define ES7210_MCLK 48 + +// dac / amp +#define HAS_I2S +#define DAC_I2S_BCK 7 +#define DAC_I2S_WS 5 +#define DAC_I2S_DOUT 6 + +// LoRa +#define USE_SX1262 +#define USE_SX1268 + +#define LORA_SCK 40 +#define LORA_MISO 38 +#define LORA_MOSI 41 +#define LORA_CS 9 + +#define LORA_DIO0 -1 // a No connect on the SX1262 module +#define LORA_RESET 17 +#define LORA_DIO1 45 // SX1262 IRQ +#define LORA_DIO2 13 // SX1262 BUSY +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled + +#define SX126X_CS LORA_CS // FIXME - we really should define LORA_CS instead +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +// 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) \ No newline at end of file diff --git a/variants/t-echo/platformio.ini b/variants/t-echo/platformio.ini new file mode 100644 index 0000000..5b295c9 --- /dev/null +++ b/variants/t-echo/platformio.ini @@ -0,0 +1,27 @@ +; First prototype eink/nrf52840/sx1262 device +[env:t-echo] +extends = nrf52840_base +board = t-echo +board_check = true +debug_tool = jlink + +# add -DCFG_SYSVIEW if you want to use the Segger systemview tool for OS profiling. +build_flags = ${nrf52840_base.build_flags} -Ivariants/t-echo + -DGPS_POWER_TOGGLE + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" + -DEINK_DISPLAY_MODEL=GxEPD2_154_D67 + -DEINK_WIDTH=200 + -DEINK_HEIGHT=200 + -DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk + -DEINK_LIMIT_FASTREFRESH=20 ; How many consecutive fast-refreshes are permitted + -DEINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates + -DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates +; -DEINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated + -DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. + +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/t-echo> +lib_deps = + ${nrf52840_base.lib_deps} + https://github.com/meshtastic/GxEPD2#55f618961db45a23eff0233546430f1e5a80f63a + lewisxhe/PCF8563_Library@^1.0.1 +;upload_protocol = fs \ No newline at end of file diff --git a/variants/t-echo/variant.cpp b/variants/t-echo/variant.cpp new file mode 100644 index 0000000..cae079b --- /dev/null +++ b/variants/t-echo/variant.cpp @@ -0,0 +1,44 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 - pins 0 and 1 are hardwired for xtal and should never be enabled + 0xff, 0xff, 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, + + // 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); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + + pinMode(PIN_LED3, OUTPUT); + ledOff(PIN_LED3); +} diff --git a/variants/t-echo/variant.h b/variants/t-echo/variant.h new file mode 100644 index 0000000..9abb4ea --- /dev/null +++ b/variants/t-echo/variant.h @@ -0,0 +1,229 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_TTGO_EINK_V1_0_ +#define _VARIANT_TTGO_EINK_V1_0_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +#define TTGO_T_ECHO + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (0 + 14) // 13 red (confirmed on 1.0 board) +// Unused(by firmware) LEDs: +#define PIN_LED2 (0 + 15) // 14 blue +#define PIN_LED3 (0 + 13) // 15 green + +#define LED_RED PIN_LED3 +#define LED_BLUE PIN_LED1 +#define LED_GREEN PIN_LED2 + +#define LED_BUILTIN LED_BLUE +#define LED_CONN PIN_GREEN + +#define LED_STATE_ON 0 // State when LED is lit + +/* + * 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_BUTTON_TOUCH (0 + 11) // 0.11 is the soft touch button on T-Echo + +#define BUTTON_CLICK_MS 400 +#define BUTTON_TOUCH_MS 200 + +/* + * Analog pins + */ +#define PIN_A0 (4) // Battery ADC + +#define BATTERY_PIN PIN_A0 + +static const uint8_t A0 = PIN_A0; + +#define ADC_RESOLUTION 14 + +#define PIN_NFC1 (9) +#define PIN_NFC2 (10) + +/* + * Serial interfaces + */ + +/* +No longer populated on PCB +*/ +// #define PIN_SERIAL2_RX (0 + 6) +// #define PIN_SERIAL2_TX (0 + 8) +// #define PIN_SERIAL2_EN (0 + 17) + +/** + Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (26) +#define PIN_WIRE_SCL (27) + +/* touch sensor, active high */ + +#define TP_SER_IO (0 + 11) + +#define PIN_RTC_INT (0 + 16) // Interrupt from the PCF8563 RTC + +/* +External serial flash WP25R1635FZUIL0 +*/ + +// QSPI Pins +#define PIN_QSPI_SCK (32 + 14) +#define PIN_QSPI_CS (32 + 15) +#define PIN_QSPI_IO0 (32 + 12) // MOSI if using two bit interface +#define PIN_QSPI_IO1 (32 + 13) // MISO if using two bit interface +#define PIN_QSPI_IO2 (0 + 7) // WP if using two bit interface (i.e. not used) +#define PIN_QSPI_IO3 (0 + 5) // HOLD if using two bit interface (i.e. not used) + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES MX25R1635F +#define EXTERNAL_FLASH_USE_QSPI + +/* + * Lora radio + */ + +#define USE_SX1262 +#define USE_SX1268 +#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 +// CPU? +#define SX126X_BUSY (0 + 17) +#define SX126X_RESET (0 + 25) +// 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) + +// #define LORA_DISABLE_SENDING // Define this to disable transmission for testing (power testing etc...) + +// #undef SX126X_CS + +/* + * eink display pins + */ + +#define PIN_EINK_EN (32 + 11) // Note: this is really just backlight power +#define PIN_EINK_CS (0 + 30) +#define PIN_EINK_BUSY (0 + 3) +#define PIN_EINK_DC (0 + 28) +#define PIN_EINK_RES (0 + 2) +#define PIN_EINK_SCLK (0 + 31) +#define PIN_EINK_MOSI (0 + 29) // also called SDI + +// Controls power for all peripherals (eink + GPS + LoRa + Sensor) +#define PIN_POWER_EN (0 + 12) +// #define PIN_POWER_EN1 (0 + 13) + +#define USE_EINK + +#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 + +/* + * GPS pins + */ + +#define GPS_L76K +#define PIN_GPS_REINIT (32 + 5) // An output to reset L76K GPS. As per datasheet, low for > 100ms will reset the L76K + +#define PIN_GPS_STANDBY (32 + 2) // An output to wake GPS, low means allow sleep, high means force wake +// Seems to be missing on this new board +// #define PIN_GPS_PPS (32 + 4) // Pulse per second input from the GPS +#define GPS_TX_PIN (32 + 9) // This is for bits going TOWARDS the CPU +#define GPS_RX_PIN (32 + 8) // This is for bits going TOWARDS the GPS + +#define GPS_THREAD_INTERVAL 50 + +#define PIN_SERIAL1_RX GPS_TX_PIN +#define PIN_SERIAL1_TX GPS_RX_PIN + +// PCF8563 RTC Module +#define PCF8563_RTC 0x51 + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +// For LORA, spi 0 +#define PIN_SPI_MISO (0 + 23) +#define PIN_SPI_MOSI (0 + 22) +#define PIN_SPI_SCK (0 + 19) + +#define PIN_PWR_EN (0 + 6) + +// To debug via the segger JLINK console rather than the CDC-ACM serial device +// #define USE_SEGGER + +// Battery +// The battery sense is hooked to pin A0 (4) +// it is defined in the anlaolgue pin section of this file +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER (2.0F) + +#define HAS_RTC 1 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif \ No newline at end of file diff --git a/variants/t-watch-s3/pins_arduino.h b/variants/t-watch-s3/pins_arduino.h new file mode 100644 index 0000000..35f0e93 --- /dev/null +++ b/variants/t-watch-s3/pins_arduino.h @@ -0,0 +1,56 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +// static const uint8_t LED_BUILTIN = -1; + +// static const uint8_t TX = 43; +// static const uint8_t RX = 44; + +static const uint8_t SDA = 10; +static const uint8_t SCL = 11; + +// Default SPI will be mapped to Radio +static const uint8_t SS = 5; +static const uint8_t MOSI = 1; +static const uint8_t MISO = 4; +static const uint8_t SCK = 3; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +#endif /* Pins_Arduino_h */ \ No newline at end of file diff --git a/variants/t-watch-s3/platformio.ini b/variants/t-watch-s3/platformio.ini new file mode 100644 index 0000000..1f5fc27 --- /dev/null +++ b/variants/t-watch-s3/platformio.ini @@ -0,0 +1,18 @@ +; LilyGo T-Watch S3 +[env:t-watch-s3] +extends = esp32s3_base +board = t-watch-s3 +board_check = true +upload_protocol = esptool + +build_flags = ${esp32_base.build_flags} + -DT_WATCH_S3 + -Ivariants/t-watch-s3 + -DPCF8563_RTC=0x51 + +lib_deps = ${esp32s3_base.lib_deps} + lovyan03/LovyanGFX@^1.1.9 + lewisxhe/PCF8563_Library@1.0.1 + adafruit/Adafruit DRV2605 Library@^1.2.2 + earlephilhower/ESP8266Audio@^1.9.7 + earlephilhower/ESP8266SAM@^1.0.1 \ No newline at end of file diff --git a/variants/t-watch-s3/variant.h b/variants/t-watch-s3/variant.h new file mode 100644 index 0000000..9f939d8 --- /dev/null +++ b/variants/t-watch-s3/variant.h @@ -0,0 +1,73 @@ +// ST7789 TFT LCD +#define ST7789_CS 12 +#define ST7789_RS 38 // DC +#define ST7789_SDA 13 // MOSI +#define ST7789_SCK 18 +#define ST7789_RESET -1 +#define ST7789_MISO -1 +#define ST7789_BUSY -1 +#define ST7789_BL 45 +#define ST7789_SPI_HOST SPI3_HOST +#define TFT_BL 45 +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 16000000 +#define TFT_HEIGHT 240 +#define TFT_WIDTH 240 +#define TFT_OFFSET_X 0 +#define TFT_OFFSET_Y 0 +#define TFT_OFFSET_ROTATION 2 +#define SCREEN_ROTATE +#define SCREEN_TRANSITION_FRAMERATE 5 // fps + +#define HAS_TOUCHSCREEN 1 +#define SCREEN_TOUCH_INT 16 +#define SCREEN_TOUCH_USE_I2C1 +#define TOUCH_I2C_PORT 1 +#define TOUCH_SLAVE_ADDRESS 0x38 + +#define SLEEP_TIME 180 + +#define I2C_SDA1 39 // Used for capacitive touch +#define I2C_SCL1 40 // Used for capacitive touch + +#define HAS_I2S +#define DAC_I2S_BCK 48 +#define DAC_I2S_WS 15 +#define DAC_I2S_DOUT 46 + +#define HAS_AXP2101 + +#define HAS_RTC 1 + +#define I2C_SDA 10 // For QMC6310 sensors and screens +#define I2C_SCL 11 // For QMC6310 sensors and screens + +#define BMA4XX_INT 14 // Interrupt for BMA_423 axis sensor + +#define HAS_GPS 0 +#undef GPS_RX_PIN +#undef GPS_TX_PIN + +#define USE_SX1262 +#define USE_SX1268 + +#define LORA_SCK 3 +#define LORA_MISO 4 +#define LORA_MOSI 1 +#define LORA_CS 5 + +#define LORA_DIO0 -1 // a No connect on the SX1262 module +#define LORA_RESET 8 +#define LORA_DIO1 9 // SX1262 IRQ +#define LORA_DIO2 7 // SX1262 BUSY +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled + +#define SX126X_CS LORA_CS // FIXME - we really should define LORA_CS instead +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +// 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) diff --git a/variants/tbeam-s3-core/pins_arduino.h b/variants/tbeam-s3-core/pins_arduino.h new file mode 100644 index 0000000..e66b69e --- /dev/null +++ b/variants/tbeam-s3-core/pins_arduino.h @@ -0,0 +1,42 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// Now declared in .platformio/packages/framework-arduinoespressif32/cores/esp32/Arduino.h +// #define NUM_ANALOG_INPUTS 20 +// #define EXTERNAL_NUM_INTERRUPTS 46 +// #define NUM_DIGITAL_PINS 48 +// #define analogInputToDigitalPin(p) (((p) < 20) ? (analogChannelToDigitalPin(p)) : -1) +// #define digitalPinToInterrupt(p) (((p) < 48) ? (p) : -1) +// #define digitalPinHasPWM(p) (p < 46) + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +// The default Wire will be mapped to PMU and RTC +static const uint8_t SDA = 42; +static const uint8_t SCL = 41; + +// Default SPI will be mapped to Radio +static const uint8_t SS = 10; +static const uint8_t MOSI = 11; +static const uint8_t MISO = 13; +static const uint8_t SCK = 12; + +// Another SPI bus shares SD card and QMI8653 inertial measurement sensor +#define SPI_MOSI (35) +#define SPI_SCK (36) +#define SPI_MISO (37) +#define SPI_CS (47) +#define IMU_CS (34) + +#define SDCARD_CS SPI_CS +#define IMU_INT (33) +// #define PMU_IRQ (40) +#define RTC_INT (14) + +#endif /* Pins_Arduino_h */ \ No newline at end of file diff --git a/variants/tbeam-s3-core/platformio.ini b/variants/tbeam-s3-core/platformio.ini new file mode 100644 index 0000000..e50d506 --- /dev/null +++ b/variants/tbeam-s3-core/platformio.ini @@ -0,0 +1,14 @@ +; The 1.0 release of the LilyGo TBEAM-S3-Core board +[env:tbeam-s3-core] +extends = esp32s3_base +board = tbeam-s3-core +board_check = true + +lib_deps = + ${esp32s3_base.lib_deps} + lewisxhe/PCF8563_Library@1.0.1 + +build_flags = + ${esp32s3_base.build_flags} + -Ivariants/tbeam-s3-core + -DPCF8563_RTC=0x51 ;Putting definitions in variant.h does not compile correctly diff --git a/variants/tbeam-s3-core/variant.h b/variants/tbeam-s3-core/variant.h new file mode 100644 index 0000000..cc70645 --- /dev/null +++ b/variants/tbeam-s3-core/variant.h @@ -0,0 +1,69 @@ +// #define BUTTON_NEED_PULLUP // if set we need to turn on the internal CPU pullup during sleep + +#define I2C_SDA1 42 // Used for PMU management and PCF8563 +#define I2C_SCL1 41 // Used for PMU management and PCF8563 + +#define I2C_SDA 17 // For QMC6310 sensors and screens +#define I2C_SCL 18 // For QMC6310 sensors and screens + +#define BUTTON_PIN 0 // The middle button GPIO on the T-Beam S3 +// #define BUTTON_PIN_ALT 13 // Alternate GPIO for an external button if needed. Does anyone use this? It is not documented +// anywhere. +// #define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module. + +#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 +#define USE_SX1262 +#define USE_SX1268 + +#define LORA_DIO0 -1 // a No connect on the SX1262 module +#define LORA_RESET 5 +#define LORA_DIO1 1 // SX1262 IRQ +#define LORA_DIO2 4 // SX1262 BUSY +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled + +#ifdef USE_SX1262 +#define SX126X_CS 10 // FIXME - we really should define LORA_CS instead +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +// 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) +#endif + +// Leave undefined to disable our PMU IRQ handler. DO NOT ENABLE THIS because the pmuirq can cause sperious interrupts +// and waking from light sleep +// #define PMU_IRQ 40 +#define HAS_AXP2101 + +#define HAS_RTC 1 + +// Specify the PMU as Wire1. In the t-beam-s3 core, PCF8563 and PMU share the bus +#define PMU_USE_WIRE1 +#define RTC_USE_WIRE1 + +#define LORA_SCK 12 +#define LORA_MISO 13 +#define LORA_MOSI 11 +#define LORA_CS 10 + +#define GPS_RX_PIN 9 +#define GPS_TX_PIN 8 +#define GPS_WAKEUP_PIN 7 +#define GPS_1PPS_PIN 6 + +#define HAS_SDCARD // Have SPI interface SD card slot +#define SDCARD_USE_SPI1 + +// PCF8563 RTC Module +// #define PCF8563_RTC 0x51 //Putting definitions in variant. h does not compile correctly + +// has 32768 Hz crystal +#define HAS_32768HZ + +#define USE_SH1106 \ No newline at end of file diff --git a/variants/tbeam/platformio.ini b/variants/tbeam/platformio.ini new file mode 100644 index 0000000..85e66c2 --- /dev/null +++ b/variants/tbeam/platformio.ini @@ -0,0 +1,11 @@ +; The 1.0 release of the TBEAM board +[env:tbeam] +extends = esp32_base +board = ttgo-t-beam +board_check = true +lib_deps = + ${esp32_base.lib_deps} +build_flags = + ${esp32_base.build_flags} -D TBEAM_V10 -I variants/tbeam + -DGPS_POWER_TOGGLE ; comment this line to disable double press function on the user button to turn off gps entirely. +upload_speed = 921600 \ No newline at end of file diff --git a/variants/tbeam/variant.h b/variants/tbeam/variant.h new file mode 100644 index 0000000..8771c20 --- /dev/null +++ b/variants/tbeam/variant.h @@ -0,0 +1,45 @@ +// #define BUTTON_NEED_PULLUP // if set we need to turn on the internal CPU pullup during sleep + +#define I2C_SDA 21 +#define I2C_SCL 22 + +#define BUTTON_PIN 38 // The middle button GPIO on the T-Beam +// #define BUTTON_PIN_ALT 13 // Alternate GPIO for an external button if needed. Does anyone use this? It is not documented +// anywhere. +#define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module. + +#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 +#define USE_RF95 // RFM95/SX127x +#define USE_SX1262 +#define USE_SX1268 + +#define LORA_DIO0 26 // a No connect on the SX1262 module +#define LORA_RESET 23 +#define LORA_DIO1 33 // SX1262 IRQ +#define LORA_DIO2 32 // SX1262 BUSY +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS // FIXME - we really should define LORA_CS instead +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +// 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) +#endif + +// Leave undefined to disable our PMU IRQ handler. DO NOT ENABLE THIS because the pmuirq can cause sperious interrupts +// and waking from light sleep +// #define PMU_IRQ 35 +#define HAS_AXP192 +#define GPS_UBLOX +#define GPS_RX_PIN 34 +#define GPS_TX_PIN 12 +// #define GPS_DEBUG \ No newline at end of file diff --git a/variants/tbeam_v07/platformio.ini b/variants/tbeam_v07/platformio.ini new file mode 100644 index 0000000..0cba924 --- /dev/null +++ b/variants/tbeam_v07/platformio.ini @@ -0,0 +1,7 @@ +; The original TBEAM board without the AXP power chip and a few other changes +[env:tbeam0_7] +board_level = extra +extends = esp32_base +board = ttgo-t-beam +build_flags = + ${esp32_base.build_flags} -D TBEAM_V07 -I variants/tbeam_v07 \ No newline at end of file diff --git a/variants/tbeam_v07/variant.h b/variants/tbeam_v07/variant.h new file mode 100644 index 0000000..898705c --- /dev/null +++ b/variants/tbeam_v07/variant.h @@ -0,0 +1,22 @@ +// #define BUTTON_NEED_PULLUP // if set we need to turn on the internal CPU pullup during sleep + +#define I2C_SDA 21 +#define I2C_SCL 22 + +#define BUTTON_PIN 39 +#define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +#define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module. +#define ADC_CHANNEL ADC1_GPIO35_CHANNEL + +#define USE_RF95 +#define LORA_DIO0 26 // a No connect on the SX1262 module +#define LORA_RESET 23 +#define LORA_DIO1 33 +#define LORA_DIO2 32 // Not really used + +// This board has different GPS pins than all other boards +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define GPS_RX_PIN 12 +#define GPS_TX_PIN 15 +#define GPS_UBLOX \ No newline at end of file diff --git a/variants/tlora_c6/platformio.ini b/variants/tlora_c6/platformio.ini new file mode 100644 index 0000000..d042cd7 --- /dev/null +++ b/variants/tlora_c6/platformio.ini @@ -0,0 +1,10 @@ +[env:tlora-c6] +extends = esp32c6_base +board = esp32-c6-devkitm-1 +build_flags = + ${esp32c6_base.build_flags} + -D TLORA_C6 + -I variants/tlora_c6 + -DARDUINO_USB_CDC_ON_BOOT=1 + -DARDUINO_USB_MODE=1 + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/esp32c3" \ No newline at end of file diff --git a/variants/tlora_c6/variant.h b/variants/tlora_c6/variant.h new file mode 100644 index 0000000..55635fe --- /dev/null +++ b/variants/tlora_c6/variant.h @@ -0,0 +1,21 @@ +#define I2C_SDA 8 // I2C pins for this board +#define I2C_SCL 9 + +#define LED_PIN 7 // If defined we will blink this LED +#define LED_STATE_ON 0 // State when LED is lit + +#define USE_SX1262 +#define LORA_SCK 6 +#define LORA_MISO 1 +#define LORA_MOSI 0 +#define LORA_CS 18 +#define LORA_RESET 21 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 23 +#define SX126X_DIO2 20 +#define SX126X_BUSY 22 +#define SX126X_RESET LORA_RESET +#define SX126X_RXEN 15 +#define SX126X_TXEN 14 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 diff --git a/variants/tlora_t3s3_epaper/pins_arduino.h b/variants/tlora_t3s3_epaper/pins_arduino.h new file mode 100644 index 0000000..ca44959 --- /dev/null +++ b/variants/tlora_t3s3_epaper/pins_arduino.h @@ -0,0 +1,26 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// The default Wire will be mapped to PMU and RTC +static const uint8_t SDA = 18; +static const uint8_t SCL = 12; // t3s3 e-Paper has no pin 17 as t3s3 v1, so use another free pin next to it + +// Default SPI will be mapped to Radio +static const uint8_t SS = 7; +static const uint8_t MOSI = 6; +static const uint8_t MISO = 3; +static const uint8_t SCK = 5; + +#define SPI_MOSI (11) +#define SPI_SCK (14) +#define SPI_MISO (2) +#define SPI_CS (13) + +#define SDCARD_CS SPI_CS + +#endif /* Pins_Arduino_h */ diff --git a/variants/tlora_t3s3_epaper/platformio.ini b/variants/tlora_t3s3_epaper/platformio.ini new file mode 100644 index 0000000..ceb4fba --- /dev/null +++ b/variants/tlora_t3s3_epaper/platformio.ini @@ -0,0 +1,23 @@ +[env:tlora-t3s3-epaper] +extends = esp32s3_base +board = tlora-t3s3-v1 +board_check = true +upload_protocol = esptool + +build_flags = + ${esp32_base.build_flags} -D TLORA_T3S3_EPAPER -I variants/tlora_t3s3_epaper + -DGPS_POWER_TOGGLE + -DEINK_DISPLAY_MODEL=GxEPD2_213_BN + -DEINK_WIDTH=250 + -DEINK_HEIGHT=122 + -DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk + -DEINK_LIMIT_FASTREFRESH=10 ; How many consecutive fast-refreshes are permitted + -DEINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates + -DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates + -DEINK_HASQUIRK_VICIOUSFASTREFRESH ; Identify that pixels drawn by fast-refresh are harder to clear + ;-DEINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated + ;-DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. + +lib_deps = + ${esp32s3_base.lib_deps} + https://github.com/meshtastic/GxEPD2#b202ebfec6a4821e098cf7a625ba0f6f2400292d diff --git a/variants/tlora_t3s3_epaper/variant.h b/variants/tlora_t3s3_epaper/variant.h new file mode 100644 index 0000000..461ce0c --- /dev/null +++ b/variants/tlora_t3s3_epaper/variant.h @@ -0,0 +1,69 @@ +#define HAS_SDCARD +#define SDCARD_USE_SPI1 + +// Display (E-Ink) +#define USE_EINK +#define PIN_EINK_CS 15 +#define PIN_EINK_BUSY 48 +#define PIN_EINK_DC 16 +#define PIN_EINK_RES 47 +#define PIN_EINK_SCLK 14 +#define PIN_EINK_MOSI 11 + +#define BATTERY_PIN 1 // A battery voltage measurement pin, voltage divider connected here to +// measure battery voltage ratio of voltage divider = 2.0 (assumption) +#define ADC_MULTIPLIER 2.11 // 2.0 + 10% for correction of display undervoltage. +#define ADC_CHANNEL ADC1_GPIO1_CHANNEL + +#define I2C_SDA SDA +#define I2C_SCL SCL + +// external qwiic connector +#define GPS_RX_PIN 44 +#define GPS_TX_PIN 43 + +#define LED_PIN 37 +#define BUTTON_PIN 0 +#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 +#define USE_RF95 // RFM95/SX127x +#define USE_SX1262 +#define USE_SX1280 + +#define LORA_SCK 5 +#define LORA_MISO 3 +#define LORA_MOSI 6 +#define LORA_CS 7 +#define LORA_RESET 8 + +// per SX1276_Receive_Interrupt/utilities.h +#define LORA_DIO0 9 +#define LORA_DIO1 33 // TCXO_EN ? +#define LORA_DIO2 34 +#define LORA_RXEN 21 +#define LORA_TXEN 10 + +// per SX1262_Receive_Interrupt/utilities.h +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 33 +#define SX126X_BUSY 34 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif + +// per SX128x_Receive_Interrupt/utilities.h +#ifdef USE_SX1280 +#define SX128X_CS LORA_CS +#define SX128X_DIO1 9 +#define SX128X_DIO2 33 +#define SX128X_DIO3 34 +#define SX128X_BUSY 36 +#define SX128X_RESET LORA_RESET +#define SX128X_RXEN 21 +#define SX128X_TXEN 10 +#define SX128X_MAX_POWER 3 +#endif diff --git a/variants/tlora_t3s3_v1/pins_arduino.h b/variants/tlora_t3s3_v1/pins_arduino.h new file mode 100644 index 0000000..4ced1b4 --- /dev/null +++ b/variants/tlora_t3s3_v1/pins_arduino.h @@ -0,0 +1,26 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// The default Wire will be mapped to PMU and RTC +static const uint8_t SDA = 18; +static const uint8_t SCL = 17; + +// Default SPI will be mapped to Radio +static const uint8_t SS = 7; +static const uint8_t MOSI = 6; +static const uint8_t MISO = 3; +static const uint8_t SCK = 5; + +#define SPI_MOSI (11) +#define SPI_SCK (14) +#define SPI_MISO (2) +#define SPI_CS (13) + +#define SDCARD_CS SPI_CS + +#endif /* Pins_Arduino_h */ diff --git a/variants/tlora_t3s3_v1/platformio.ini b/variants/tlora_t3s3_v1/platformio.ini new file mode 100644 index 0000000..0a57972 --- /dev/null +++ b/variants/tlora_t3s3_v1/platformio.ini @@ -0,0 +1,9 @@ +[env:tlora-t3s3-v1] +extends = esp32s3_base +board = tlora-t3s3-v1 +board_check = true +upload_protocol = esptool + +build_flags = + ${esp32_base.build_flags} -D TLORA_T3S3_V1 -I variants/tlora_t3s3_v1 + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. \ No newline at end of file diff --git a/variants/tlora_t3s3_v1/rfswitch.h b/variants/tlora_t3s3_v1/rfswitch.h new file mode 100644 index 0000000..19080ce --- /dev/null +++ b/variants/tlora_t3s3_v1/rfswitch.h @@ -0,0 +1,11 @@ +#include "RadioLib.h" + +static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; + +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, +}; \ No newline at end of file diff --git a/variants/tlora_t3s3_v1/variant.h b/variants/tlora_t3s3_v1/variant.h new file mode 100644 index 0000000..babe44a --- /dev/null +++ b/variants/tlora_t3s3_v1/variant.h @@ -0,0 +1,79 @@ +#define HAS_SDCARD +#define SDCARD_USE_SPI1 + +#define USE_SSD1306 + +#define BATTERY_PIN 1 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +// ratio of voltage divider = 2.0 (R42=100k, R43=100k) +#define ADC_MULTIPLIER 2.11 // 2.0 + 10% for correction of display undervoltage. +#define ADC_CHANNEL ADC1_GPIO1_CHANNEL + +#define I2C_SDA 18 // I2C pins for this board +#define I2C_SCL 17 + +#define I2C_SDA1 43 +#define I2C_SCL1 44 + +#define LED_PIN 37 // If defined we will blink this LED +#define BUTTON_PIN 0 // If defined, this will be used for user button presses, + +#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 +#define USE_RF95 // RFM95/SX127x +#define USE_SX1262 +#define USE_SX1280 +#define USE_LR1121 + +#define LORA_SCK 5 +#define LORA_MISO 3 +#define LORA_MOSI 6 +#define LORA_CS 7 +#define LORA_RESET 8 + +// per SX1276_Receive_Interrupt/utilities.h +#define LORA_DIO0 9 +#define LORA_DIO1 33 // TCXO_EN ? +#define LORA_DIO2 34 +#define LORA_RXEN 21 +#define LORA_TXEN 10 + +// per SX1262_Receive_Interrupt/utilities.h +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 33 +#define SX126X_BUSY 34 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif + +// per SX128x_Receive_Interrupt/utilities.h +#ifdef USE_SX1280 +#define SX128X_CS LORA_CS +#define SX128X_DIO1 9 +#define SX128X_DIO2 33 +#define SX128X_DIO3 34 +#define SX128X_BUSY 36 +#define SX128X_RESET LORA_RESET +#define SX128X_RXEN 21 +#define SX128X_TXEN 10 +#define SX128X_MAX_POWER 3 +#endif + +// LR1121 +#ifdef USE_LR1121 +#define LR1121_IRQ_PIN 36 +#define LR1121_NRESET_PIN LORA_RESET +#define LR1121_BUSY_PIN LORA_DIO2 +#define LR1121_SPI_NSS_PIN LORA_CS +#define LR1121_SPI_SCK_PIN LORA_SCK +#define LR1121_SPI_MOSI_PIN LORA_MOSI +#define LR1121_SPI_MISO_PIN LORA_MISO +#define LR11X0_DIO3_TCXO_VOLTAGE 3.0 +#define LR11X0_DIO_AS_RF_SWITCH +#endif + +#define HAS_SDCARD // Have SPI interface SD card slot +#define SDCARD_USE_SPI1 \ No newline at end of file diff --git a/variants/tlora_v1/platformio.ini b/variants/tlora_v1/platformio.ini new file mode 100644 index 0000000..65ec4bc --- /dev/null +++ b/variants/tlora_v1/platformio.ini @@ -0,0 +1,6 @@ +[env:tlora-v1] +board_level = extra +extends = esp32_base +board = ttgo-lora32-v1 +build_flags = + ${esp32_base.build_flags} -D TLORA_V1 -I variants/tlora_v1 \ No newline at end of file diff --git a/variants/tlora_v1/variant.h b/variants/tlora_v1/variant.h new file mode 100644 index 0000000..83e2c19 --- /dev/null +++ b/variants/tlora_v1/variant.h @@ -0,0 +1,17 @@ +#define I2C_SDA 4 // I2C pins for this board +#define I2C_SCL 15 + +#define RESET_OLED 16 // If defined, this pin will be used to reset the display controller + +#define VEXT_ENABLE 21 // active low, powers the oled display and the lora antenna boost +#define VEXT_ON_VALUE LOW +#define LED_PIN 2 // If defined we will blink this LED +#define BUTTON_PIN 0 // If defined, this will be used for user button presses +#define BUTTON_NEED_PULLUP +#define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module. + +#define USE_RF95 +#define LORA_DIO0 26 // a No connect on the SX1262 module +#define LORA_RESET 14 +#define LORA_DIO1 33 // Must be manually wired: https://www.thethingsnetwork.org/forum/t/big-esp32-sx127x-topic-part-3/18436 +#define LORA_DIO2 32 // Not really used \ No newline at end of file diff --git a/variants/tlora_v1_3/platformio.ini b/variants/tlora_v1_3/platformio.ini new file mode 100644 index 0000000..99df28e --- /dev/null +++ b/variants/tlora_v1_3/platformio.ini @@ -0,0 +1,6 @@ +[env:tlora_v1_3] +board_level = extra +extends = esp32_base +board = ttgo-lora32-v1 +build_flags = + ${esp32_base.build_flags} -D TLORA_V1_3 -I variants/tlora_v1_3 \ No newline at end of file diff --git a/variants/tlora_v1_3/variant.h b/variants/tlora_v1_3/variant.h new file mode 100644 index 0000000..73cb31f --- /dev/null +++ b/variants/tlora_v1_3/variant.h @@ -0,0 +1,18 @@ +#define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +#define ADC_CHANNEL ADC1_GPIO35_CHANNEL + +#define I2C_SDA 21 // I2C pins for this board +#define I2C_SCL 22 + +#define RESET_OLED 16 // If defined, this pin will be used to reset the display controller + +#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 36 +#define BUTTON_NEED_PULLUP + +#define USE_RF95 +#define LORA_DIO0 26 // a No connect on the SX1262 module +#define LORA_RESET 14 +#define LORA_DIO1 33 // Prob. must be manually wired: https://www.thethingsnetwork.org/forum/t/big-esp32-sx127x-topic-part-3/18436 +#define LORA_DIO2 32 // Not really used \ No newline at end of file diff --git a/variants/tlora_v2/platformio.ini b/variants/tlora_v2/platformio.ini new file mode 100644 index 0000000..8087a30 --- /dev/null +++ b/variants/tlora_v2/platformio.ini @@ -0,0 +1,6 @@ +[env:tlora-v2] +board_level = extra +extends = esp32_base +board = ttgo-lora32-v1 +build_flags = + ${esp32_base.build_flags} -D TLORA_V2 -I variants/tlora_v2 \ No newline at end of file diff --git a/variants/tlora_v2/variant.h b/variants/tlora_v2/variant.h new file mode 100644 index 0000000..8a7cf89 --- /dev/null +++ b/variants/tlora_v2/variant.h @@ -0,0 +1,18 @@ +#define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +#define ADC_CHANNEL ADC1_GPIO35_CHANNEL + +#define I2C_SDA 21 // I2C pins for this board +#define I2C_SCL 22 + +#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_NEED_PULLUP + +#define USE_RF95 +#define LORA_DIO0 26 // a No connect on the SX1262 module +#define LORA_RESET 14 +#define LORA_DIO1 33 // Must be manually wired: https://www.thethingsnetwork.org/forum/t/big-esp32-sx127x-topic-part-3/18436 +#define LORA_DIO2 32 // Not really used \ No newline at end of file diff --git a/variants/tlora_v2_1_16/platformio.ini b/variants/tlora_v2_1_16/platformio.ini new file mode 100644 index 0000000..351f716 --- /dev/null +++ b/variants/tlora_v2_1_16/platformio.ini @@ -0,0 +1,7 @@ +[env:tlora-v2-1-1_6] +extends = esp32_base +board = ttgo-lora32-v21 +board_check = true +build_flags = + ${esp32_base.build_flags} -D TLORA_V2_1_16 -I variants/tlora_v2_1_16 + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. \ No newline at end of file diff --git a/variants/tlora_v2_1_16/variant.h b/variants/tlora_v2_1_16/variant.h new file mode 100644 index 0000000..8bb5ce3 --- /dev/null +++ b/variants/tlora_v2_1_16/variant.h @@ -0,0 +1,28 @@ +#define BATTERY_PIN 35 +#define ADC_CHANNEL ADC1_GPIO35_CHANNEL +#define BATTERY_SENSE_SAMPLES 30 + +// ratio of voltage divider = 2.0 (R42=100k, R43=100k) +#define ADC_MULTIPLIER 2 + +#define I2C_SDA 21 // I2C pins for this board +#define I2C_SCL 22 + +#define LED_PIN 25 // If defined we will blink this LED +#define BUTTON_PIN 12 // If defined, this will be used for user button presses, + +#define BUTTON_NEED_PULLUP + +#define USE_RF95 +#define LORA_DIO0 26 // a No connect on the SX1262 module +#define LORA_RESET 23 + +// In the T3 V1.6.1 TXCO version, GPIO 33 is connected to Radio’s +// internal temperature-compensated crystal oscillator enable +#ifdef LORA_TCXO_GPIO +#define LORA_DIO1 RADIOLIB_NC // no-connect on sx127x module +#else +#define LORA_DIO1 33 // https://www.thethingsnetwork.org/forum/t/big-esp32-sx127x-topic-part-3/18436 +#endif + +#define LORA_DIO2 32 // Not really used \ No newline at end of file diff --git a/variants/tlora_v2_1_16_tcxo/platformio.ini b/variants/tlora_v2_1_16_tcxo/platformio.ini new file mode 100644 index 0000000..e54c1a9 --- /dev/null +++ b/variants/tlora_v2_1_16_tcxo/platformio.ini @@ -0,0 +1,9 @@ +[env:tlora-v2-1-1_6-tcxo] +extends = esp32_base +board = ttgo-lora32-v21 +build_flags = + ${esp32_base.build_flags} + -D TLORA_V2_1_16 + -I variants/tlora_v2_1_16 + -D GPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + -D LORA_TCXO_GPIO=33 \ No newline at end of file diff --git a/variants/tlora_v2_1_18/platformio.ini b/variants/tlora_v2_1_18/platformio.ini new file mode 100644 index 0000000..36d6a31 --- /dev/null +++ b/variants/tlora_v2_1_18/platformio.ini @@ -0,0 +1,6 @@ +[env:tlora-v2-1-1_8] +extends = esp32_base +board = ttgo-lora32-v21 + +build_flags = + ${esp32_base.build_flags} -D TLORA_V2_1_18 -I variants/tlora_v2_1_18 \ No newline at end of file diff --git a/variants/tlora_v2_1_18/variant.h b/variants/tlora_v2_1_18/variant.h new file mode 100644 index 0000000..efc6769 --- /dev/null +++ b/variants/tlora_v2_1_18/variant.h @@ -0,0 +1,20 @@ +#define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +// ratio of voltage divider = 2.0 (R42=100k, R43=100k) +#define ADC_MULTIPLIER 2.11 // 2.0 + 10% for correction of display undervoltage. +#define ADC_CHANNEL ADC1_GPIO35_CHANNEL + +#define I2C_SDA 21 // I2C pins for this board +#define I2C_SCL 22 + +#define LED_PIN 25 // If defined we will blink this LED +#define BUTTON_PIN 12 // If defined, this will be used for user button presses, + +#define BUTTON_NEED_PULLUP + +#define USE_SX1280 +#define LORA_RESET 23 + +#define SX128X_CS 18 +#define SX128X_DIO1 26 +#define SX128X_BUSY 32 +#define SX128X_RESET LORA_RESET \ No newline at end of file diff --git a/variants/tracker-t1000-e/platformio.ini b/variants/tracker-t1000-e/platformio.ini new file mode 100644 index 0000000..0758116 --- /dev/null +++ b/variants/tracker-t1000-e/platformio.ini @@ -0,0 +1,14 @@ +[env:tracker-t1000-e] +extends = nrf52840_base +board = tracker-t1000-e +build_flags = ${nrf52840_base.build_flags} -Ivariants/tracker-t1000-e -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 -DTRACKER_T1000_E + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" + -DGPS_POWER_TOGGLE +board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/tracker-t1000-e> +lib_deps = + ${nrf52840_base.lib_deps} + https://github.com/meshtastic/QMA6100P_Arduino_Library.git#14c900b8b2e4feaac5007a7e41e0c1b7f0841136 +debug_tool = jlink +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +upload_protocol = nrfutil diff --git a/variants/tracker-t1000-e/rfswitch.h b/variants/tracker-t1000-e/rfswitch.h new file mode 100644 index 0000000..e229d77 --- /dev/null +++ b/variants/tracker-t1000-e/rfswitch.h @@ -0,0 +1,13 @@ +#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 Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 DIO7 DIO8 + {LR11x0::MODE_STBY, {LOW, LOW, LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW, LOW, HIGH}}, + {LR11x0::MODE_TX, {HIGH, HIGH, LOW, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH, LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW, LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW, HIGH, LOW}}, + {LR11x0::MODE_WIFI, {LOW, LOW, LOW, LOW}}, END_OF_MODE_TABLE, +}; \ No newline at end of file diff --git a/variants/tracker-t1000-e/variant.cpp b/variants/tracker-t1000-e/variant.cpp new file mode 100644 index 0000000..8096705 --- /dev/null +++ b/variants/tracker-t1000-e/variant.cpp @@ -0,0 +1,64 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 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, + + // 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); + + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_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(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, LOW); + + pinMode(GPS_VRTC_EN, OUTPUT); + digitalWrite(GPS_VRTC_EN, HIGH); + + pinMode(PIN_GPS_RESET, OUTPUT); + digitalWrite(PIN_GPS_RESET, LOW); + + pinMode(GPS_SLEEP_INT, OUTPUT); + digitalWrite(GPS_SLEEP_INT, HIGH); + + pinMode(GPS_RTC_INT, OUTPUT); + digitalWrite(GPS_RTC_INT, LOW); + + pinMode(GPS_RESETB_OUT, INPUT); +} \ No newline at end of file diff --git a/variants/tracker-t1000-e/variant.h b/variants/tracker-t1000-e/variant.h new file mode 100644 index 0000000..6a1f996 --- /dev/null +++ b/variants/tracker-t1000-e/variant.h @@ -0,0 +1,162 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_TRACKER_T1000_E_ +#define _VARIANT_TRACKER_T1000_E_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// Use the native nrf52 usb power detection +#define NRF_APM + +#define PIN_3V3_EN (32 + 6) // P1.6, Power to Sensors +#define PIN_3V3_ACC_EN (32 + 7) // P1.7, Power to Acc + +#define PIN_LED1 (0 + 24) // P0.24 +#define LED_PIN PIN_LED1 +#define LED_BUILTIN -1 +#define LED_BLUE -1 // Actually green +#define LED_STATE_ON 1 // State when LED is lit + +#define BUTTON_PIN (0 + 6) // P0.06 +#define BUTTON_ACTIVE_LOW false +#define BUTTON_ACTIVE_PULLUP false +#define BUTTON_SENSE_TYPE 0x6 + +#define HAS_WIRE 1 + +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (0 + 26) // P0.26 +#define PIN_WIRE_SCL (0 + 27) // P0.27 +#define I2C_NO_RESCAN // I2C is a bit finicky, don't scan too much +#define HAS_QMA6100P // very rare beast, only on this board. +#define QMA_6100P_INT_PIN (32 + 2) // P1.02 + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (0 + 14) // P0.14 +#define PIN_SERIAL1_TX (0 + 13) // P0.13 + +#define PIN_SERIAL2_RX (0 + 17) // P0.17 +#define PIN_SERIAL2_TX (0 + 16) // P0.16 + +#define SPI_INTERFACES_COUNT 1 + +#define PIN_SPI_MISO (32 + 8) // P1.08 +#define PIN_SPI_MOSI (32 + 9) // P1.09 +#define PIN_SPI_SCK (0 + 11) // P0.11 +#define PIN_SPI_NSS (0 + 12) // P0.12 + +#define LORA_RESET (32 + 10) // P1.10 // RST +#define LORA_DIO1 (32 + 1) // P1.01 // IRQ +#define LORA_DIO2 (0 + 7) // P0.07 // BUSY +#define LORA_SCK PIN_SPI_SCK +#define LORA_MISO PIN_SPI_MISO +#define LORA_MOSI PIN_SPI_MOSI +#define LORA_CS PIN_SPI_NSS + +// supported modules list +#define USE_LR1110 + +#define LR1110_IRQ_PIN LORA_DIO1 +#define LR1110_NRESET_PIN LORA_RESET +#define LR1110_BUSY_PIN LORA_DIO2 +#define LR1110_SPI_NSS_PIN LORA_CS +#define LR1110_SPI_SCK_PIN LORA_SCK +#define LR1110_SPI_MOSI_PIN LORA_MOSI +#define LR1110_SPI_MISO_PIN LORA_MISO + +#define LR11X0_DIO3_TCXO_VOLTAGE 1.6 +#define LR11X0_DIO_AS_RF_SWITCH + +#define HAS_GPS 1 +#define GNSS_AIROHA +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +#define GPS_BAUDRATE 115200 + +#define PIN_GPS_EN (32 + 11) // P1.11 +#define GPS_EN_ACTIVE HIGH + +#define PIN_GPS_RESET (32 + 15) // P1.15 +#define GPS_RESET_MODE HIGH + +#define GPS_VRTC_EN (0 + 8) // P0.8, always high +#define GPS_SLEEP_INT (32 + 12) // P1.12, always high +#define GPS_RTC_INT (0 + 15) // P0.15, normal is LOW, wake by HIGH +#define GPS_RESETB_OUT (32 + 14) // P1.14, always input pull_up + +#define GPS_FIX_HOLD_TIME 15000 // ms +#define BATTERY_PIN 2 // P0.02/AIN0, BAT_ADC +#define BATTERY_IMMUTABLE +#define ADC_MULTIPLIER (2.0F) +// P0.04/AIN2 is VCC_ADC, P0.05/AIN3 is CHARGER_DET, P1.03 is CHARGE_STA, P1.04 is CHARGE_DONE + +#define EXT_CHRG_DETECT (32 + 3) // P1.03 +#define EXT_CHRG_DETECT_VALUE LOW +// #define EXT_IS_CHRGD (32 + 4) // P1.04 +// #define EXT_IS_CHRGD_VALUE LOW +#define EXT_PWR_DETECT (0 + 5) // P0.05 + +#define ADC_RESOLUTION 14 +#define BATTERY_SENSE_RESOLUTION_BITS 12 + +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 + +// Buzzer +#define BUZZER_EN_PIN (32 + 5) // P1.05, always high +#define PIN_BUZZER (0 + 25) // P0.25, pwm output + +#define T1000X_SENSOR_EN +#define T1000X_VCC_PIN (0 + 4) // P0.4 +#define T1000X_NTC_PIN (0 + 31) // P0.31/AIN7 +#define T1000X_LUX_PIN (0 + 29) // P0.29/AIN5 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif // _VARIANT_TRACKER_T1000_E_ \ No newline at end of file diff --git a/variants/trackerd/platformio.ini b/variants/trackerd/platformio.ini new file mode 100644 index 0000000..6fba190 --- /dev/null +++ b/variants/trackerd/platformio.ini @@ -0,0 +1,13 @@ +[env:trackerd] +extends = esp32_base +;platform = https://github.com/platformio/platform-espressif32.git#feature/arduino-upstream +platform = espressif32 +board = pico32 +board_build.f_flash = 80000000L + +build_flags = + ${esp32_base.build_flags} -D PRIVATE_HW -I variants/trackerd -D BSFILE=\"boards/dragino_lbt2.h\" +;board_build.partitions = no_ota.csv +;platform_packages = +; platformio/framework-arduinoespressif32@3 +;platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.1-RC1 diff --git a/variants/trackerd/variant.h b/variants/trackerd/variant.h new file mode 100644 index 0000000..c4dfb9e --- /dev/null +++ b/variants/trackerd/variant.h @@ -0,0 +1,53 @@ +// Initialize i2c bus on sd_dat and esp_led pins, respectively. We need a bus to not hang on boot +#define HAS_SCREEN 0 +#define I2C_SDA 21 +#define I2C_SCL 22 + +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define GPS_RX_PIN 9 +#define GPS_TX_PIN 10 + +#define LED_PIN 13 // 13 red, 2 blue, 15 red + +// #define HAS_BUTTON 0 +#define BUTTON_PIN 0 +#define BUTTON_NEED_PULLUP + +#define USE_RF95 +#define LORA_DIO0 26 // a No connect on the SX1262 module +#define LORA_RESET 23 +#define LORA_DIO1 33 +#define LORA_DIO2 32 // Not really used + +#undef BAT_MEASURE_ADC_UNIT +#define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +#define ADC_MULTIPLIER 1.34 // tracked resistance divider is 100k+470k, so it can not fillfull well on esp32 adc +#define ADC_CHANNEL ADC1_GPIO35_CHANNEL +#define ADC_ATTENUATION ADC_ATTEN_DB_12 // lower dB for high resistance voltage divider + +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#undef PIN_GPS_PPS + +#define PIN_GPS_EN 12 +#define GPS_EN_ACTIVE 1 + +#define GPS_TX_PIN 10 +#define GPS_RX_PIN 9 + +#define PIN_GPS_RESET 25 +// #define PIN_GPS_REINIT 25 +#define GPS_RESET_MODE 1 + +#define GPS_L76K + +#undef PIN_LED1 +#undef PIN_LED2 +#undef PIN_LED3 + +#define PIN_LED1 13 +#define PIN_LED2 15 +#define PIN_LED3 2 + +#define ledOff(pin) pinMode(pin, INPUT) \ No newline at end of file diff --git a/variants/tracksenger/internal/pins_arduino.h b/variants/tracksenger/internal/pins_arduino.h new file mode 100644 index 0000000..1052af9 --- /dev/null +++ b/variants/tracksenger/internal/pins_arduino.h @@ -0,0 +1,72 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include "soc/soc_caps.h" +#include + +#define WIFI_LoRa_32_V3 true +#define DISPLAY_HEIGHT 80 +#define DISPLAY_WIDTH 160 + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +static const uint8_t LED_BUILTIN = 18; +#define BUILTIN_LED LED_BUILTIN // backward compatibility +#define LED_BUILTIN LED_BUILTIN + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 45; +static const uint8_t SCL = 46; + +static const uint8_t SS = 8; +static const uint8_t MOSI = 10; +static const uint8_t MISO = 11; +static const uint8_t SCK = 9; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +static const uint8_t Vext = 36; +static const uint8_t LED = 18; + +static const uint8_t RST_LoRa = 12; +static const uint8_t BUSY_LoRa = 13; +static const uint8_t DIO0 = 14; + +#endif /* Pins_Arduino_h */ diff --git a/variants/tracksenger/internal/variant.h b/variants/tracksenger/internal/variant.h new file mode 100644 index 0000000..57ead84 --- /dev/null +++ b/variants/tracksenger/internal/variant.h @@ -0,0 +1,92 @@ +#define LED_PIN 18 + +#define HELTEC_TRACKER_V1_X + +// TRACKSENGER builtin LCD + +// I2C +#define I2C_SDA SDA +#define I2C_SCL SCL + +// ST7735S TFT LCD +#define ST7735S 1 // there are different (sub-)versions of ST7735 +#define ST7735_CS 38 +#define ST7735_RS 40 // DC +#define ST7735_SDA 42 // MOSI +#define ST7735_SCK 41 +#define ST7735_RESET 39 +#define ST7735_MISO -1 +#define ST7735_BUSY -1 +#define TFT_BL 21 /* V1.1 PCB marking */ +#define ST7735_SPI_HOST SPI3_HOST +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 16000000 +#define SCREEN_ROTATE +#define TFT_HEIGHT DISPLAY_WIDTH +#define TFT_WIDTH DISPLAY_HEIGHT +#define TFT_OFFSET_X 26 +#define TFT_OFFSET_Y -1 +#define SCREEN_TRANSITION_FRAMERATE 3 // fps +#define DISPLAY_FORCE_SMALL_FONTS + +#define VEXT_ENABLE 3 // active HIGH, powers the lora antenna boost +#define VEXT_ON_VALUE HIGH +#define BUTTON_PIN 0 + +#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_ATTENUATION ADC_ATTEN_DB_2_5 // lower dB for high resistance voltage divider +#define ADC_MULTIPLIER 4.9 +#define ADC_CTRL 2 // active HIGH, powers the voltage divider. Only on 1.1 +#define ADC_CTRL_ENABLED HIGH + +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define GPS_RX_PIN 33 +#define GPS_TX_PIN 34 +#define PIN_GPS_RESET 35 +#define PIN_GPS_PPS 36 + +#define GPS_RESET_MODE LOW +#define GPS_UC6580 +#define GPS_BAUDRATE 115200 + +#define USE_SX1262 +#define LORA_DIO0 -1 // a No connect on the SX1262 module +#define LORA_RESET 12 +#define LORA_DIO1 14 // SX1262 IRQ +#define LORA_DIO2 13 // SX1262 BUSY +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled + +#define LORA_SCK 9 +#define LORA_MISO 11 +#define LORA_MOSI 10 +#define LORA_CS 8 + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET + +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// Picomputer gets a white on black display +#define TFT_MESH COLOR565(0xFF, 0xFF, 0xFF) + +// keyboard changes + +#define PIN_BUZZER 43 +#define CANNED_MESSAGE_MODULE_ENABLE 1 + +#define INPUTBROKER_MATRIX_TYPE 1 + +#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/tracksenger/lcd/pins_arduino.h b/variants/tracksenger/lcd/pins_arduino.h new file mode 100644 index 0000000..1052af9 --- /dev/null +++ b/variants/tracksenger/lcd/pins_arduino.h @@ -0,0 +1,72 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include "soc/soc_caps.h" +#include + +#define WIFI_LoRa_32_V3 true +#define DISPLAY_HEIGHT 80 +#define DISPLAY_WIDTH 160 + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +static const uint8_t LED_BUILTIN = 18; +#define BUILTIN_LED LED_BUILTIN // backward compatibility +#define LED_BUILTIN LED_BUILTIN + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 45; +static const uint8_t SCL = 46; + +static const uint8_t SS = 8; +static const uint8_t MOSI = 10; +static const uint8_t MISO = 11; +static const uint8_t SCK = 9; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +static const uint8_t Vext = 36; +static const uint8_t LED = 18; + +static const uint8_t RST_LoRa = 12; +static const uint8_t BUSY_LoRa = 13; +static const uint8_t DIO0 = 14; + +#endif /* Pins_Arduino_h */ diff --git a/variants/tracksenger/lcd/variant.h b/variants/tracksenger/lcd/variant.h new file mode 100644 index 0000000..ecf4e85 --- /dev/null +++ b/variants/tracksenger/lcd/variant.h @@ -0,0 +1,116 @@ +#define LED_PIN 18 + +#define HELTEC_TRACKER_V1_X + +// TRACKSENGER 2.8" IPS 320x240 + +// I2C +// #define I2C_SDA 42 +// #define I2C_SCL 41 +// #define HAS_SCREEN 1 +// #define USE_SSD1306 + +// Default SPI1 will be mapped to the display +#define ST7789_SDA 42 +#define ST7789_SCK 41 +#define ST7789_CS 38 +#define ST7789_RS 40 +#define ST7789_BL 21 +// P#define TFT_BL 21 /* V1.1 PCB marking */ + +#define ST7789_RESET -1 +#define ST7789_MISO -1 +#define ST7789_BUSY -1 +#define ST7789_SPI_HOST SPI3_HOST +#define TFT_BL 21 +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 16000000 +#define TFT_HEIGHT 320 +#define TFT_WIDTH 240 +#define TFT_OFFSET_X 0 +#define TFT_OFFSET_Y 0 +#define TFT_OFFSET_ROTATION 0 +#define SCREEN_ROTATE + +// ST7735S TFT LCD +// #define ST7735S 1 // there are different (sub-)versions of ST7735 +// #define ST7735_CS 38 +// #define ST7735_RS 40 // DC +// #define ST7735_SDA 42 // MOSI +// #define ST7735_SCK 41 +// #define ST7735_RESET 39 +// #define ST7735_MISO -1 +// #define ST7735_BUSY -1 +#define TFT_BL 21 /* V1.1 PCB marking */ +// #define ST7735_SPI_HOST SPI3_HOST +// #define SPI_FREQUENCY 40000000 +// #define SPI_READ_FREQUENCY 16000000 +// #define SCREEN_ROTATE +// #define TFT_HEIGHT DISPLAY_WIDTH +// #define TFT_WIDTH DISPLAY_HEIGHT +// #define TFT_OFFSET_X 26 +// #define TFT_OFFSET_Y -1 +#define SCREEN_TRANSITION_FRAMERATE 3 // fps +// #define DISPLAY_FORCE_SMALL_FONTS + +#define VEXT_ENABLE 3 // active HIGH, powers the lora antenna boost +#define VEXT_ON_VALUE HIGH +#define BUTTON_PIN 0 + +#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_ATTENUATION ADC_ATTEN_DB_2_5 // lower dB for high resistance voltage divider +#define ADC_MULTIPLIER 4.9 +#define ADC_CTRL 2 // active HIGH, powers the voltage divider. Only on 1.1 +#define ADC_CTRL_ENABLED HIGH + +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define GPS_RX_PIN 33 +#define GPS_TX_PIN 34 +#define PIN_GPS_RESET 35 +#define PIN_GPS_PPS 36 + +#define GPS_RESET_MODE LOW +#define GPS_UC6580 +#define GPS_BAUDRATE 115200 + +#define USE_SX1262 +#define LORA_DIO0 -1 // a No connect on the SX1262 module +#define LORA_RESET 12 +#define LORA_DIO1 14 // SX1262 IRQ +#define LORA_DIO2 13 // SX1262 BUSY +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled + +#define LORA_SCK 9 +#define LORA_MISO 11 +#define LORA_MOSI 10 +#define LORA_CS 8 + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET + +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// Picomputer gets a white on black display +#define TFT_MESH COLOR565(0xFF, 0xFF, 0xFF) + +// keyboard changes + +#define PIN_BUZZER 43 +#define CANNED_MESSAGE_MODULE_ENABLE 1 + +#define INPUTBROKER_MATRIX_TYPE 1 + +#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/tracksenger/oled/pins_arduino.h b/variants/tracksenger/oled/pins_arduino.h new file mode 100644 index 0000000..1052af9 --- /dev/null +++ b/variants/tracksenger/oled/pins_arduino.h @@ -0,0 +1,72 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include "soc/soc_caps.h" +#include + +#define WIFI_LoRa_32_V3 true +#define DISPLAY_HEIGHT 80 +#define DISPLAY_WIDTH 160 + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +static const uint8_t LED_BUILTIN = 18; +#define BUILTIN_LED LED_BUILTIN // backward compatibility +#define LED_BUILTIN LED_BUILTIN + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 45; +static const uint8_t SCL = 46; + +static const uint8_t SS = 8; +static const uint8_t MOSI = 10; +static const uint8_t MISO = 11; +static const uint8_t SCK = 9; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +static const uint8_t Vext = 36; +static const uint8_t LED = 18; + +static const uint8_t RST_LoRa = 12; +static const uint8_t BUSY_LoRa = 13; +static const uint8_t DIO0 = 14; + +#endif /* Pins_Arduino_h */ diff --git a/variants/tracksenger/oled/variant.h b/variants/tracksenger/oled/variant.h new file mode 100644 index 0000000..70f0f32 --- /dev/null +++ b/variants/tracksenger/oled/variant.h @@ -0,0 +1,94 @@ +#define LED_PIN 18 + +#define HELTEC_TRACKER_V1_X + +// TRACKSENGER 2.42" I2C OLED + +// I2C +#define I2C_SDA 42 +#define I2C_SCL 41 +#define HAS_SCREEN 1 +#define USE_SSD1306 + +// ST7735S TFT LCD +// #define ST7735S 1 // there are different (sub-)versions of ST7735 +// #define ST7735_CS 38 +// #define ST7735_RS 40 // DC +// #define ST7735_SDA 42 // MOSI +// #define ST7735_SCK 41 +// #define ST7735_RESET 39 +// #define ST7735_MISO -1 +// #define ST7735_BUSY -1 +#define TFT_BL 21 /* V1.1 PCB marking */ +// #define ST7735_SPI_HOST SPI3_HOST +// #define SPI_FREQUENCY 40000000 +// #define SPI_READ_FREQUENCY 16000000 +// #define SCREEN_ROTATE +// #define TFT_HEIGHT DISPLAY_WIDTH +// #define TFT_WIDTH DISPLAY_HEIGHT +// #define TFT_OFFSET_X 26 +// #define TFT_OFFSET_Y -1 +#define SCREEN_TRANSITION_FRAMERATE 3 // fps +// #define DISPLAY_FORCE_SMALL_FONTS + +#define VEXT_ENABLE 3 // active HIGH, powers the lora antenna boost +#define VEXT_ON_VALUE HIGH +#define BUTTON_PIN 0 + +#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_ATTENUATION ADC_ATTEN_DB_2_5 // lower dB for high resistance voltage divider +#define ADC_MULTIPLIER 4.9 +#define ADC_CTRL 2 // active HIGH, powers the voltage divider. Only on 1.1 +#define ADC_CTRL_ENABLED HIGH + +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define GPS_RX_PIN 33 +#define GPS_TX_PIN 34 +#define PIN_GPS_RESET 35 +#define PIN_GPS_PPS 36 + +#define GPS_RESET_MODE LOW +#define GPS_UC6580 +#define GPS_BAUDRATE 115200 + +#define USE_SX1262 +#define LORA_DIO0 -1 // a No connect on the SX1262 module +#define LORA_RESET 12 +#define LORA_DIO1 14 // SX1262 IRQ +#define LORA_DIO2 13 // SX1262 BUSY +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled + +#define LORA_SCK 9 +#define LORA_MISO 11 +#define LORA_MOSI 10 +#define LORA_CS 8 + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET + +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// Picomputer gets a white on black display +#define TFT_MESH COLOR565(0xFF, 0xFF, 0xFF) + +// keyboard changes + +#define PIN_BUZZER 43 +#define CANNED_MESSAGE_MODULE_ENABLE 1 + +#define INPUTBROKER_MATRIX_TYPE 1 + +#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/tracksenger/platformio.ini b/variants/tracksenger/platformio.ini new file mode 100644 index 0000000..d3e3126 --- /dev/null +++ b/variants/tracksenger/platformio.ini @@ -0,0 +1,40 @@ +[env:tracksenger] +extends = esp32s3_base +board = heltec_wireless_tracker +upload_protocol = esp-builtin + +build_flags = + ${esp32s3_base.build_flags} -I variants/tracksenger/internal + -D HELTEC_TRACKER_V1_1 + -D GPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + ;-D DEBUG_DISABLED ; uncomment this line to disable DEBUG output + +lib_deps = + ${esp32s3_base.lib_deps} + lovyan03/LovyanGFX@^1.1.8 + +[env:tracksenger-lcd] +extends = esp32s3_base +board = heltec_wireless_tracker +upload_protocol = esp-builtin + +build_flags = + ${esp32s3_base.build_flags} -I variants/tracksenger/lcd + -D HELTEC_TRACKER_V1_1 + -D GPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + ;-D DEBUG_DISABLED ; uncomment this line to disable DEBUG output + +lib_deps = + ${esp32s3_base.lib_deps} + lovyan03/LovyanGFX@^1.1.8 + +[env:tracksenger-oled] +extends = esp32s3_base +board = heltec_wireless_tracker +upload_protocol = esp-builtin + +build_flags = + ${esp32s3_base.build_flags} -I variants/tracksenger/oled + -D HELTEC_TRACKER_V1_1 + -D GPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + ;-D DEBUG_DISABLED ; uncomment this line to disable DEBUG output diff --git a/variants/unphone/pins_arduino.h b/variants/unphone/pins_arduino.h new file mode 100644 index 0000000..c4e9add --- /dev/null +++ b/variants/unphone/pins_arduino.h @@ -0,0 +1,67 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x16D0 +#define USB_PID 0x1178 + +#define EXTERNAL_NUM_INTERRUPTS 46 +#define NUM_DIGITAL_PINS 48 +#define NUM_ANALOG_INPUTS 20 + +#define analogInputToDigitalPin(p) (((p) < 20) ? (analogChannelToDigitalPin(p)) : -1) +#define digitalPinToInterrupt(p) (((p) < 48) ? (p) : -1) +#define digitalPinHasPWM(p) (p < 46) + +#define LED_BUILTIN 13 +#define BUILTIN_LED LED_BUILTIN // backward compatibility + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 3; +static const uint8_t SCL = 4; + +static const uint8_t SS = 13; +static const uint8_t MOSI = 40; +static const uint8_t MISO = 41; +static const uint8_t SCK = 39; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 8; +static const uint8_t A3 = 9; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 14; +static const uint8_t A7 = 7; +static const uint8_t A8 = 15; +static const uint8_t A9 = 33; +static const uint8_t A10 = 27; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 2; +static const uint8_t T2 = 8; +static const uint8_t T3 = 9; +static const uint8_t T4 = 5; +static const uint8_t T5 = 6; +static const uint8_t T6 = 14; +static const uint8_t T7 = 7; +static const uint8_t T8 = 15; +static const uint8_t T9 = 33; +static const uint8_t T10 = 27; +static const uint8_t T11 = 12; +static const uint8_t T12 = 13; +static const uint8_t T13 = 14; +static const uint8_t T14 = 15; + +#endif /* Pins_Arduino_h */ diff --git a/variants/unphone/platformio.ini b/variants/unphone/platformio.ini new file mode 100644 index 0000000..489c70f --- /dev/null +++ b/variants/unphone/platformio.ini @@ -0,0 +1,77 @@ +; platformio.ini for unphone meshtastic + +[env:unphone] + +extends = esp32s3_base +board = unphone9 +upload_speed = 921600 +monitor_speed = 115200 +monitor_filters = esp32_exception_decoder + +build_unflags = + ${esp32s3_base.build_unflags} + -D ARDUINO_USB_MODE + +build_flags = ${esp32_base.build_flags} + -D UNPHONE + -I variants/unphone + -D ARDUINO_USB_MODE=0 + -D UNPHONE_ACCEL=0 + -D UNPHONE_TOUCHS=0 + -D UNPHONE_SDCARD=0 + -D UNPHONE_UI0=0 + -D UNPHONE_LORA=0 + -D UNPHONE_FACTORY_MODE=0 + +build_src_filter = ${esp32_base.build_src_filter} +<../variants/unphone> + +lib_deps = ${esp32s3_base.lib_deps} + lovyan03/LovyanGFX@ 1.1.12 + https://gitlab.com/hamishcunningham/unphonelibrary#meshtastic@9.0.0 + adafruit/Adafruit NeoPixel @ ^1.12.0 + + +[env:unphone-tft] +extends = esp32s3_base +board_level = extra +board = unphone +board_build.partitions = default_8MB.csv +monitor_speed = 115200 +monitor_filters = esp32_exception_decoder +build_flags = ${esp32_base.build_flags} + -D UNPHONE + -D UNPHONE_ACCEL=0 + -D UNPHONE_TOUCHS=0 + -D UNPHONE_SDCARD=0 + -D UNPHONE_UI0=0 + -D UNPHONE_LORA=0 + -D UNPHONE_FACTORY_MODE=0 + -D MAX_THREADS=40 + -D HAS_SCREEN=0 + -D HAS_TFT=1 + -D RAM_SIZE=512 + -D LV_LVGL_H_INCLUDE_SIMPLE + -D LV_CONF_INCLUDE_SIMPLE + -D LV_COMP_CONF_INCLUDE_SIMPLE + -D LV_BUILD_TEST=0 + -D LV_USE_PERF_MONITOR=0 + -D LV_USE_MEM_MONITOR=0 + -D USE_LOG_DEBUG + -D LOG_DEBUG_INC=\"DebugConfiguration.h\" +; -D CALIBRATE_TOUCH=0 + -D LGFX_DRIVER=LGFX_UNPHONE_V9 + -D VIEW_320x240 +; -D USE_DOUBLE_BUFFER + -D USE_PACKET_API + -I lib/device-ui/generated/ui_320x240 + -I variants/unphone + +build_src_filter = ${esp32_base.build_src_filter} +<../variants/unphone> + +<../lib/device-ui/generated/ui_320x240> + +<../lib/device-ui/resources> + +<../lib/device-ui/source> + +lib_deps = ${esp32s3_base.lib_deps} + lovyan03/LovyanGFX@^1.1.12 + https://gitlab.com/hamishcunningham/unphonelibrary#meshtastic@9.0.0 + adafruit/Adafruit NeoPixel@1.12.0 \ No newline at end of file diff --git a/variants/unphone/variant.cpp b/variants/unphone/variant.cpp new file mode 100644 index 0000000..7884f82 --- /dev/null +++ b/variants/unphone/variant.cpp @@ -0,0 +1,21 @@ +// meshtastic/firmware/variants/unphone/variant.cpp + +#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 + + 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/unphone/variant.h b/variants/unphone/variant.h new file mode 100644 index 0000000..0a94c59 --- /dev/null +++ b/variants/unphone/variant.h @@ -0,0 +1,71 @@ +// meshtastic/firmware/variants/unphone/variant.h + +#pragma once + +#define SPI_SCK 39 +#define SPI_MOSI 40 +#define SPI_MISO 41 + +// We use the RFM95W LoRa module +#define USE_RF95 +#define LORA_SCK SPI_SCK +#define LORA_MOSI SPI_MOSI +#define LORA_MISO SPI_MISO +#define LORA_CS 44 +#define LORA_DIO0 10 // AKA LORA_IRQ +#define LORA_RESET 42 +#define LORA_DIO1 11 +#define LORA_DIO2 RADIOLIB_NC // Not really used + +// HX8357 TFT LCD +#define HX8357_CS 48 +#define HX8357_RS 47 // AKA DC +#define HX8357_RESET 46 +#define HX8357_SCK SPI_SCK +#define HX8357_MOSI SPI_MOSI +#define HX8357_MISO SPI_MISO +#define HX8357_BUSY -1 +#define HX8357_SPI_HOST SPI2_HOST +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 16000000 +#define TFT_HEIGHT 480 +#define TFT_WIDTH 320 +#define TFT_OFFSET_X 0 +#define TFT_OFFSET_Y 0 +#define TFT_OFFSET_ROTATION 6 // unPhone's screen wired unusually, 0 typical +#define TFT_INVERT false +#define SCREEN_ROTATE true +#define SCREEN_TRANSITION_FRAMERATE 5 + +#define HAS_TOUCHSCREEN 1 +#define USE_XPT2046 1 +#define TOUCH_CS 38 + +#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 + +// #define HAS_SDCARD 1 // causes hang if defined +#define SDCARD_CS 43 + +#define LED_PIN 13 // the red part of the RGB LED +#define LED_STATE_ON 0 // State when LED is lit + +#define BUTTON_PIN 21 // Button 3 - square - top button in landscape mode +#define BUTTON_NEED_PULLUP // we do need a helping hand up +#define BUTTON_PIN_ALT 45 // Button 1 - triangle - bottom button in landscape mode + +#define I2C_SDA 3 // I2C pins for this board +#define I2C_SCL 4 + +#define LSM6DS3_WAKE_THRESH 5 // higher values reduce the sensitivity of the wake threshold + +// ratio of voltage divider = 3.20 (R1=100k, R2=220k) +// #define ADC_MULTIPLIER 3.2 + +// #define BATTERY_PIN 13 // battery V measurement pin; vbat divider is here +// #define ADC_CHANNEL ADC2_GPIO13_CHANNEL +// #define BAT_MEASURE_ADC_UNIT 2 \ No newline at end of file diff --git a/variants/wio-e5/platformio.ini b/variants/wio-e5/platformio.ini new file mode 100644 index 0000000..29c4a0a --- /dev/null +++ b/variants/wio-e5/platformio.ini @@ -0,0 +1,36 @@ +[env:wio-e5] +extends = stm32_base +board = lora_e5_dev_board +build_flags = + ${stm32_base.build_flags} + -Ivariants/wio-e5 + -DSERIAL_UART_INSTANCE=1 + -DPIN_SERIAL_RX=PB7 + -DPIN_SERIAL_TX=PB6 + -DHAL_DAC_MODULE_ONLY + -DHAL_ADC_MODULE_DISABLED + -DHAL_COMP_MODULE_DISABLED + -DHAL_CRC_MODULE_DISABLED + -DHAL_CRYP_MODULE_DISABLED + -DHAL_GTZC_MODULE_DISABLED + -DHAL_HSEM_MODULE_DISABLED + -DHAL_I2C_MODULE_DISABLED + -DHAL_I2S_MODULE_DISABLED + -DHAL_IPCC_MODULE_DISABLED + -DHAL_IRDA_MODULE_DISABLED + -DHAL_IWDG_MODULE_DISABLED + -DHAL_LPTIM_MODULE_DISABLED + -DHAL_PKA_MODULE_DISABLED + -DHAL_RNG_MODULE_DISABLED + -DHAL_RTC_MODULE_DISABLED + -DHAL_SMARTCARD_MODULE_DISABLED + -DHAL_SMBUS_MODULE_DISABLED + -DHAL_TIM_MODULE_DISABLED + -DHAL_WWDG_MODULE_DISABLED + -DHAL_EXTI_MODULE_DISABLED + -DRADIOLIB_EXCLUDE_SX128X=1 + -DRADIOLIB_EXCLUDE_SX127X=1 + -DRADIOLIB_EXCLUDE_LR11X0=1 +; -D PIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF + +upload_port = stlink \ No newline at end of file diff --git a/variants/wio-e5/variant.h b/variants/wio-e5/variant.h new file mode 100644 index 0000000..ac92915 --- /dev/null +++ b/variants/wio-e5/variant.h @@ -0,0 +1,21 @@ +/* +Wio-E5 mini (formerly LoRa-E5 mini) +https://www.seeedstudio.com/LoRa-E5-mini-STM32WLE5JC-p-4869.html +https://www.seeedstudio.com/LoRa-E5-Wireless-Module-p-4745.html +*/ + +/* +This variant is a work in progress. +Do not expect a working Meshtastic device with this target. +*/ + +#ifndef _VARIANT_WIOE5_ +#define _VARIANT_WIOE5_ + +#define USE_STM32WLx +#define MAX_NUM_NODES 10 + +#define LED_PIN PB5 +#define LED_STATE_ON 1 + +#endif diff --git a/variants/wio-sdk-wm1110/platformio.ini b/variants/wio-sdk-wm1110/platformio.ini new file mode 100644 index 0000000..e77455b --- /dev/null +++ b/variants/wio-sdk-wm1110/platformio.ini @@ -0,0 +1,25 @@ +; The black Wio-WM1110 Dev Kit with sensors and the WM1110 module +[env:wio-sdk-wm1110] +extends = nrf52840_base +board = wio-sdk-wm1110 + +extra_scripts = + bin/platformio-custom.py + extra_scripts/disable_adafruit_usb.py + +# Remove adafruit USB serial from the build (it is incompatible with using the ch340 serial chip on this board) +build_unflags = ${nrf52840_base:build_unflags} -DUSBCON -DUSE_TINYUSB +build_flags = ${nrf52840_base.build_flags} -Ivariants/wio-sdk-wm1110 -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 -DWIO_WM1110 + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + -DCFG_TUD_CDC=0 +board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/wio-sdk-wm1110> + +;debug_tool = jlink +debug_tool = stlink +; No need to reflash if the binary hasn't changed + +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +;upload_protocol = nrfutil +;upload_protocol = stlink \ No newline at end of file diff --git a/variants/wio-sdk-wm1110/rfswitch.h b/variants/wio-sdk-wm1110/rfswitch.h new file mode 100644 index 0000000..cda6364 --- /dev/null +++ b/variants/wio-sdk-wm1110/rfswitch.h @@ -0,0 +1,15 @@ +#include "RadioLib.h" +#include "nrf.h" + +// set RF switch configuration for Wio WM1110 +// Wio WM1110 uses DIO5 and DIO6 for RF switching + +static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; + +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, +}; diff --git a/variants/wio-sdk-wm1110/variant.cpp b/variants/wio-sdk-wm1110/variant.cpp new file mode 100644 index 0000000..5a35879 --- /dev/null +++ b/variants/wio-sdk-wm1110/variant.cpp @@ -0,0 +1,45 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 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, + + // 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); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); +} \ No newline at end of file diff --git a/variants/wio-sdk-wm1110/variant.h b/variants/wio-sdk-wm1110/variant.h new file mode 100644 index 0000000..b6e5c79 --- /dev/null +++ b/variants/wio-sdk-wm1110/variant.h @@ -0,0 +1,120 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_WIO_SDK_WM1110_ +#define _VARIANT_WIO_SDK_WM1110_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef USE_TINYUSB +#error TinyUSB must be disabled by platformio before using this variant +#endif + +// We use the hardware serial port for the serial console +#define Serial Serial1 + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_3V3_EN (0 + 7) // P0.7, Power to Sensors + +#define PIN_WIRE_SDA (0 + 27) // P0.27 +#define PIN_WIRE_SCL (0 + 26) // P0.26 + +#define PIN_LED1 (0 + 13) // P0.13 +#define PIN_LED2 (0 + 14) // P0.14 + +#define LED_BUILTIN PIN_LED1 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 // Actually red + +#define LED_STATE_ON 1 // State when LED is lit + +#define BUTTON_PIN (0 + 23) // P0.23 + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (0 + 22) // P0.22 +#define PIN_SERIAL1_TX (0 + 24) // P0.24 + +#define PIN_SERIAL2_RX (0 + 6) // P0.06 +#define PIN_SERIAL2_TX (0 + 8) // P0.08 + +#define SPI_INTERFACES_COUNT 1 + +#define PIN_SPI_MISO (32 + 15) // P1.15 47 +#define PIN_SPI_MOSI (32 + 14) // P1.14 46 +#define PIN_SPI_SCK (32 + 13) // P1.13 45 +#define PIN_SPI_NSS (32 + 12) // P1.12 44 + +#define LORA_RESET (32 + 10) // P1.10 42 // RST +#define LORA_DIO1 (32 + 8) // P1.08 40 // IRQ +#define LORA_DIO2 (32 + 11) // P1.11 43 // BUSY +#define LORA_SCK PIN_SPI_SCK +#define LORA_MISO PIN_SPI_MISO +#define LORA_MOSI PIN_SPI_MOSI +#define LORA_CS PIN_SPI_NSS + +// supported modules list +#define USE_LR1110 + +#define LR1110_IRQ_PIN LORA_DIO1 +#define LR1110_NRESET_PIN LORA_RESET +#define LR1110_BUSY_PIN LORA_DIO2 +#define LR1110_SPI_NSS_PIN LORA_CS +#define LR1110_SPI_SCK_PIN LORA_SCK +#define LR1110_SPI_MOSI_PIN LORA_MOSI +#define LR1110_SPI_MISO_PIN LORA_MISO + +#define LR11X0_DIO3_TCXO_VOLTAGE 1.6 +#define LR11X0_DIO_AS_RF_SWITCH + +#define LR1110_GNSS_ANT_PIN (32 + 5) // P1.05 37 + +#define NRF_USE_SERIAL_DFU + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif // _VARIANT_WIO_SDK_WM1110_ diff --git a/variants/wio-t1000-s/platformio.ini b/variants/wio-t1000-s/platformio.ini new file mode 100644 index 0000000..cb1cf86 --- /dev/null +++ b/variants/wio-t1000-s/platformio.ini @@ -0,0 +1,15 @@ +; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921 +[env:wio-t1000-s] +extends = nrf52840_base +board = wio-t1000-s +board_level = extra +build_flags = ${nrf52840_base.build_flags} -Ivariants/wio-t1000-s -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 -DWIO_WM1110 + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. +board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/wio-t1000-s> +lib_deps = + ${nrf52840_base.lib_deps} +debug_tool = jlink +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +upload_protocol = jlink \ No newline at end of file diff --git a/variants/wio-t1000-s/rfswitch.h b/variants/wio-t1000-s/rfswitch.h new file mode 100644 index 0000000..e229d77 --- /dev/null +++ b/variants/wio-t1000-s/rfswitch.h @@ -0,0 +1,13 @@ +#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 Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 DIO7 DIO8 + {LR11x0::MODE_STBY, {LOW, LOW, LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW, LOW, HIGH}}, + {LR11x0::MODE_TX, {HIGH, HIGH, LOW, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH, LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW, LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW, HIGH, LOW}}, + {LR11x0::MODE_WIFI, {LOW, LOW, LOW, LOW}}, END_OF_MODE_TABLE, +}; \ No newline at end of file diff --git a/variants/wio-t1000-s/variant.cpp b/variants/wio-t1000-s/variant.cpp new file mode 100644 index 0000000..85e0c44 --- /dev/null +++ b/variants/wio-t1000-s/variant.cpp @@ -0,0 +1,64 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 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, + + // 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); + + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); + + pinMode(PIN_3V3_ACC_EN, OUTPUT); + digitalWrite(PIN_3V3_ACC_EN, LOW); + + pinMode(BUZZER_EN_PIN, OUTPUT); + digitalWrite(BUZZER_EN_PIN, HIGH); + + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, LOW); + + pinMode(GPS_VRTC_EN, OUTPUT); + digitalWrite(GPS_VRTC_EN, HIGH); + + pinMode(PIN_GPS_RESET, OUTPUT); + digitalWrite(PIN_GPS_RESET, LOW); + + pinMode(GPS_SLEEP_INT, OUTPUT); + digitalWrite(GPS_SLEEP_INT, HIGH); + + pinMode(GPS_RTC_INT, OUTPUT); + digitalWrite(GPS_RTC_INT, LOW); + + pinMode(GPS_RESETB_OUT, INPUT); +} \ No newline at end of file diff --git a/variants/wio-t1000-s/variant.h b/variants/wio-t1000-s/variant.h new file mode 100644 index 0000000..eb6a34d --- /dev/null +++ b/variants/wio-t1000-s/variant.h @@ -0,0 +1,160 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_WIO_SDK_WM1110_ +#define _VARIANT_WIO_SDK_WM1110_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +#define BLE_DFU_SECURE // we use the 7.x softdevice signing keys + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +#define PIN_3V3_EN (32 + 6) // P1.6, Power to Sensors +#define PIN_3V3_ACC_EN (32 + 7) // P1.7, Power to Acc + +#define PIN_LED1 (0 + 24) // P0.24 +#define LED_PIN PIN_LED1 +#define LED_BUILTIN -1 +#define LED_BLUE -1 // Actually green +#define LED_STATE_ON 1 // State when LED is lit + +#define BUTTON_PIN (0 + 6) // P0.6 +#define BUTTON_ACTIVE_LOW false +#define BUTTON_ACTIVE_PULLUP false +#define BUTTON_SENSE_TYPE INPUT_SENSE_HIGH + +#define HAS_WIRE 0 + +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (0 + 26) // P0.26 +#define PIN_WIRE_SCL (0 + 27) // P0.27 + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (0 + 14) // P0.14 +#define PIN_SERIAL1_TX (0 + 13) // P0.13 + +#define PIN_SERIAL2_RX (0 + 17) // P0.17 +#define PIN_SERIAL2_TX (0 + 16) // P0.16 + +#define USER_DEBUG_PORT Serial2 + +#define SPI_INTERFACES_COUNT 1 + +#define PIN_SPI_MISO (32 + 8) // P1.08 +#define PIN_SPI_MOSI (32 + 9) // P1.09 +#define PIN_SPI_SCK (0 + 11) // P0.11 +#define PIN_SPI_NSS (0 + 12) // P0.12 + +#define LORA_RESET (32 + 10) // P1.10 // RST +#define LORA_DIO1 (32 + 1) // P1.01 // IRQ +#define LORA_DIO2 (0 + 7) // P0.07 // BUSY +#define LORA_SCK PIN_SPI_SCK +#define LORA_MISO PIN_SPI_MISO +#define LORA_MOSI PIN_SPI_MOSI +#define LORA_CS PIN_SPI_NSS + +// supported modules list +#define USE_LR1110 + +#define LR1110_IRQ_PIN LORA_DIO1 +#define LR1110_NRESET_PIN LORA_RESET +#define LR1110_BUSY_PIN LORA_DIO2 +#define LR1110_SPI_NSS_PIN LORA_CS +#define LR1110_SPI_SCK_PIN LORA_SCK +#define LR1110_SPI_MOSI_PIN LORA_MOSI +#define LR1110_SPI_MISO_PIN LORA_MISO + +#define LR11X0_DIO3_TCXO_VOLTAGE 1.6 +#define LR11X0_DIO_AS_RF_SWITCH + +#define HAS_GPS 1 +#define GNSS_AIROHA +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +#define GPS_BAUDRATE 115200 + +#define PIN_GPS_EN (32 + 11) // P1.11 +#define GPS_EN_ACTIVE HIGH + +#define PIN_GPS_RESET (32 + 15) // P1.15 +#define GPS_RESET_MODE HIGH + +#define GPS_VRTC_EN (0 + 8) // P0.8, awlays high +#define GPS_SLEEP_INT (32 + 12) // P1.12, awlays high +#define GPS_RTC_INT (0 + 15) // P0.15, normal is LOW, wake by HIGH +#define GPS_RESETB_OUT (32 + 14) // P1.14, awlays input pull_up + +// #define GPS_THREAD_INTERVAL 50 +#define GPS_FIX_HOLD_TIME 15000 // ms + +#define BATTERY_PIN 2 +// #define ADC_CHANNEL ADC1_GPIO2_CHANNEL +#define ADC_MULTIPLIER (2.0F) + +#define ADC_RESOLUTION 14 +#define BATTERY_SENSE_RESOLUTION_BITS 12 +// #define BATTERY_SENSE_RESOLUTION 4096.0 + +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 + +// #define ADC_CTRL (32 + 6) +// #define ADC_CTRL_ENABLED HIGH + +// Buzzer +#define BUZZER_EN_PIN (32 + 5) // P1.05, awlays high +#define PIN_BUZZER (0 + 25) // P0.25, pwm output + +#define T1000X_SENSOR_EN +#define T1000X_VCC_PIN (0 + 4) // P0.4 +#define T1000X_NTC_PIN (0 + 31) // P0.31 +#define T1000X_LUX_PIN (0 + 29) // P0.29 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif // _VARIANT_WIO_SDK_WM1110_ \ No newline at end of file diff --git a/variants/wio-tracker-wm1110/platformio.ini b/variants/wio-tracker-wm1110/platformio.ini new file mode 100644 index 0000000..614fea5 --- /dev/null +++ b/variants/wio-tracker-wm1110/platformio.ini @@ -0,0 +1,13 @@ +; The red tracker Dev Board with the WM1110 module +[env:wio-tracker-wm1110] +extends = nrf52840_base +board = wio-tracker-wm1110 +build_flags = ${nrf52840_base.build_flags} -Ivariants/wio-tracker-wm1110 -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 -DWIO_WM1110 + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. +board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/wio-tracker-wm1110> +lib_deps = + ${nrf52840_base.lib_deps} +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +;upload_protocol = jlink \ No newline at end of file diff --git a/variants/wio-tracker-wm1110/rfswitch.h b/variants/wio-tracker-wm1110/rfswitch.h new file mode 100644 index 0000000..cda6364 --- /dev/null +++ b/variants/wio-tracker-wm1110/rfswitch.h @@ -0,0 +1,15 @@ +#include "RadioLib.h" +#include "nrf.h" + +// set RF switch configuration for Wio WM1110 +// Wio WM1110 uses DIO5 and DIO6 for RF switching + +static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; + +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, +}; diff --git a/variants/wio-tracker-wm1110/variant.cpp b/variants/wio-tracker-wm1110/variant.cpp new file mode 100644 index 0000000..5a35879 --- /dev/null +++ b/variants/wio-tracker-wm1110/variant.cpp @@ -0,0 +1,45 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 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, + + // 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); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); +} \ No newline at end of file diff --git a/variants/wio-tracker-wm1110/variant.h b/variants/wio-tracker-wm1110/variant.h new file mode 100644 index 0000000..32e8448 --- /dev/null +++ b/variants/wio-tracker-wm1110/variant.h @@ -0,0 +1,114 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_WIO_TRACKER_WM1110_ +#define _VARIANT_WIO_TRACKER_WM1110_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +#define WIRE_INTERFACES_COUNT 1 + +// We rely on the nrf52840 USB controller to tell us if we are hooked to a power supply +#define NRF_APM + +#define PIN_3V3_EN (32 + 1) // P1.01, Power to Sensors + +#define PIN_WIRE_SDA (0 + 5) // P0.05 +#define PIN_WIRE_SCL (0 + 4) // P0.04 + +#define PIN_LED1 (0 + 6) // P0.06 +#define PIN_LED2 (PINS_COUNT) // P0.14 + +#define LED_BUILTIN PIN_LED1 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 + +#define LED_STATE_ON 0 + +#define BUTTON_PIN (32 + 2) // P1.02 + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (0 + 24) // P0.24 +#define PIN_SERIAL1_TX (0 + 25) // P0.25 + +#define PIN_SERIAL2_RX (0 + 6) // P0.06 +#define PIN_SERIAL2_TX (0 + 8) // P0.08 + +#define SPI_INTERFACES_COUNT 1 + +#define PIN_SPI_MISO (32 + 15) // P1.15 47 +#define PIN_SPI_MOSI (32 + 14) // P1.14 46 +#define PIN_SPI_SCK (32 + 13) // P1.13 45 +#define PIN_SPI_NSS (32 + 12) // P1.12 44 + +#define LORA_RESET (32 + 10) // P1.10 10 // RST +#define LORA_DIO1 (0 + 2) // P0.02 2 // IRQ +#define LORA_DIO2 (32 + 11) // P1.11 43 // BUSY +#define LORA_SCK PIN_SPI_SCK +#define LORA_MISO PIN_SPI_MISO +#define LORA_MOSI PIN_SPI_MOSI +#define LORA_CS PIN_SPI_NSS + +// supported modules list +#define USE_LR1110 + +#define LR1110_IRQ_PIN LORA_DIO1 +#define LR1110_NRESET_PIN LORA_RESET +#define LR1110_BUSY_PIN LORA_DIO2 +#define LR1110_SPI_NSS_PIN LORA_CS +#define LR1110_SPI_SCK_PIN LORA_SCK +#define LR1110_SPI_MOSI_PIN LORA_MOSI +#define LR1110_SPI_MISO_PIN LORA_MISO + +#define LR11X0_DIO3_TCXO_VOLTAGE 1.6 +#define LR11X0_DIO_AS_RF_SWITCH + +#define LR1110_GNSS_ANT_PIN (32 + 5) // P1.05 37 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif // _VARIANT_WIO_TRACKER_WM1110_ \ No newline at end of file diff --git a/variants/wiphone/pins_arduino.h b/variants/wiphone/pins_arduino.h new file mode 100644 index 0000000..3759219 --- /dev/null +++ b/variants/wiphone/pins_arduino.h @@ -0,0 +1,44 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +static const uint8_t TX = 1; +static const uint8_t RX = 3; + +static const uint8_t SDA = 21; +static const uint8_t SCL = 22; + +static const uint8_t SS = 5; +static const uint8_t MOSI = 23; +static const uint8_t MISO = 19; +static const uint8_t SCK = 18; + +static const uint8_t G23 = 23; +static const uint8_t G19 = 19; +static const uint8_t G18 = 18; +static const uint8_t G3 = 3; +static const uint8_t G16 = 16; +static const uint8_t G21 = 21; +static const uint8_t G2 = 2; +static const uint8_t G12 = 12; +static const uint8_t G15 = 15; +static const uint8_t G35 = 35; +static const uint8_t G36 = 36; +static const uint8_t G25 = 25; +static const uint8_t G26 = 26; +static const uint8_t G1 = 1; +static const uint8_t G17 = 17; +static const uint8_t G22 = 22; +static const uint8_t G5 = 5; +static const uint8_t G13 = 13; +static const uint8_t G0 = 0; +static const uint8_t G34 = 34; + +static const uint8_t DAC1 = 25; +static const uint8_t DAC2 = 26; + +static const uint8_t ADC1 = 35; +static const uint8_t ADC2 = 36; + +#endif /* Pins_Arduino_h */ diff --git a/variants/wiphone/platformio.ini b/variants/wiphone/platformio.ini new file mode 100644 index 0000000..0218f89 --- /dev/null +++ b/variants/wiphone/platformio.ini @@ -0,0 +1,13 @@ +[env:wiphone] +extends = esp32_base +board = wiphone +board_level = extra +monitor_filters = esp32_exception_decoder +board_build.partitions = default_16MB.csv +build_flags = + ${esp32_base.build_flags} -D WIPHONE -I variants/wiphone +lib_deps = + ${esp32_base.lib_deps} + lovyan03/LovyanGFX@^1.1.8 + sparkfun/SX1509 IO Expander@^3.0.5 + pololu/APA102@^3.0.0 \ No newline at end of file diff --git a/variants/wiphone/variant.h b/variants/wiphone/variant.h new file mode 100644 index 0000000..cfa5667 --- /dev/null +++ b/variants/wiphone/variant.h @@ -0,0 +1,62 @@ +#define I2C_SDA 15 +#define I2C_SCL 25 + +#define GPIO_EXTENDER 1509 +#define EXTENDER_FLAG 0x40 +#define EXTENDER_PIN(x) (x + EXTENDER_FLAG) + +#undef RF95_SCK +#undef RF95_MISO +#undef RF95_MOSI +#undef RF95_NSS + +#define RF95_SCK 14 +#define RF95_MISO 12 +#define RF95_MOSI 13 +#define RF95_NSS 27 + +#define USE_RF95 +#define LORA_DIO0 38 +#define LORA_RESET RADIOLIB_NC +#define LORA_DIO1 RADIOLIB_NC +#define LORA_DIO2 RADIOLIB_NC + +// This board has no GPS or Screen for now +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define NO_GPS +#define HAS_GPS 0 +#define NO_SCREEN +#define HAS_SCREEN 0 + +// Default SPI1 will be mapped to the display +#define ST7789_SDA 23 +#define ST7789_SCK 18 +#define ST7789_CS 5 +#define ST7789_RS 26 +// 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) +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 16000000 +#define TFT_HEIGHT 240 +#define TFT_WIDTH 320 +#define TFT_OFFSET_X 0 +#define TFT_OFFSET_Y 0 +#define TFT_OFFSET_ROTATION 0 +#define SCREEN_ROTATE +#define SCREEN_TRANSITION_FRAMERATE 5 + +#define I2S_MCLK_GPIO0 +#define I2S_BCK_PIN 4 // rev1.3 - 4 (wp05) +#define I2S_WS_PIN 33 +#define I2S_MOSI_PIN 21 +#define I2S_MISO_PIN 34 \ No newline at end of file diff --git a/variants/xiao_ble/README.md b/variants/xiao_ble/README.md new file mode 100644 index 0000000..6fff9cd --- /dev/null +++ b/variants/xiao_ble/README.md @@ -0,0 +1,261 @@ +# + +

+ Xiao BLE/BLE Sense + Ebyte E22-900M30S +

+ +

+ A step-by-step guide for macOS and Linux +

+ +## Introduction + +This guide will walk you through everything needed to get the Xiao BLE (or BLE Sense) running Meshtastic using an Ebyte E22-900M30S LoRa module. The combination of the E22 with an nRF52840 MCU is desirable because it allows for both very low idle (Rx) power draw and high transmit power. The Xiao BLE is a small but surprisingly well-appointed nRF52840 board, with enough GPIO for most Meshtastic applications and a built-in LiPo charger. The E22, on the other hand, is a famously inscrutable and mysterious beast. It is one of the more readily available LoRa modules capable of transmitting at 30 dBm, and includes an LNA to boost its Rx sensitivity a few dB beyond that of the SX1262. However, its documentation is relatively sparse overall, and seems to merely hint at (or completely omit) several key details regarding its functionality. Thus, much of what follows is a synthesis of my observations and inferences over the course of many hours of trial and error. + +

Acknowledgement and friendly disclaimer

+ +Huge thanks to those in the community who have forged the way with the E22, without whose hard work none of this would have been possible! (thebentern, riddick, rainer_vie, beegee-tokyo, geeksville, caveman99, Der_Bear, PlumRugOfDoom, BigCorvus, and many others.) + +
+ +Please take the conclusions here as a tentative work in progress, representing my current (and fairly limited) understanding of the E22 when paired with this particular MCU. It is my hope that this guide will be helpful to others who are interested in trying a DIY Meshtastic build, and also be subject to revision by folks with more experience and better test equipment. + +### Obligatory liability disclaimer + +This guide and all associated content is for informational purposes only. The information presented is intended for consumption only by persons having appropriate technical skill and judgement, to be used entirely at their own discretion and risk. The authors of this guide in no way provide any warranty, express or implied, toward the content herein, nor its correctness, safety, or suitability to any particular purpose. By following the instructions in this guide in part or in full, you assume all responsibility for all potential risks, including but not limited to fire, property damage, bodily injury, and death. + +### Note + +These instructions assume you are running macOS or Linux, but it should be relatively easy to translate each command for Windows. (In this case, in step 2 below, each line of `xiao_ble.sh` would also need to be converted to the equivalent Windows CLI command and run individually.) + +## 1. Update Bootloader + +The first thing you will need to do is update the Xiao BLE's bootloader. The stock bootloader is functionally very similar to the Adafruit nRF52 UF2 bootloader, but apparently not quite enough so to work with Meshtastic out of the box. + +1. Connect the Xiao BLE to your computer via USB-C. + +2. Install `adafruit-nrfutil` by following the instructions here. + +3. Open a terminal window and navigate to `firmware/variants/xiao_ble` (where `firmware` is the directory into which you have cloned the Meshtastic firmware repo). + +4. Run the following command, replacing `/dev/cu.usbmodem2101` with the serial port your Xiao BLE is connected to: + + ```bash + adafruit-nrfutil --verbose dfu serial --package xiao_nrf52840_ble_bootloader-0.7.0-22-g277a0c8_s140_7.3.0.zip --port /dev/cu.usbmodem2101 -b 115200 --singlebank --touch 1200 + ``` + +5. If all goes well, the Xiao BLE's red LED should start to pulse slowly, and you should see a new USB storage device called `XIAO-BOOT` appear under `Locations` in Finder. + +  + +## 2. PlatformIO Environment Preparation + +Before building Meshtastic for the Xiao BLE + E22, it is necessary to pull in SoftDevice 7.3.0 and its associated linker script (nrf52840_s140_v7.ld) from Seeed Studio's Arduino core. The `xiao_ble.sh` script does this. + +1. In your terminal window, run the following command: + + ```bash + sudo ./xiao_ble.sh + ``` + +  + +## 3. Build Meshtastic + +At this point, you should be able to build the firmware successfully. + +1. In VS Code, press `Command Shift P` to bring up the command palette. + +2. Search for and run the `Developer: Reload Window` command. + +3. Bring up the command palette again with `Command Shift P`. Search for and run the `PlatformIO: Pick Project Environment` command. + +4. In the list of environments, select `env:xiao_ble`. PlatformIO may update itself for a minute or two, and should let you know once done. + +5. Return to the command palette once again (`Command Shift P`). Search for and run the `PlatformIO: Build` command. + +6. PlatformIO will build the project. After a few minutes you should see a green `SUCCESS` message. + +  + +## 4. Wire the board + +Connecting the E22 to the Xiao BLE is straightforward, but there are a few gotchas to be mindful of. + +- On the Xiao BLE: + + - Pins D4 and D5 are currently mapped to `PIN_WIRE_SDA` and `PIN_WIRE_SCL`, respectively. If you are not using I²C and would like to free up pins D4 and D5 for use as GPIO, `PIN_WIRE_SDA` and `PIN_WIRE_SCL` can be reassigned to any two other unused pin numbers. + + - Pins D6 and D7 were originally mapped to the TX and RX pins for serial interface 1 (`PIN_SERIAL1_RX` and `PIN_SERIAL1_TX`) but are currently set to -1 in `variant.h`. If you need to expose a serial interface, you can restore these pins and move e.g. `SX126X_RXEN` to pin 4 or 5 (the opposite should work too). + +- On the E22: + + - There are two options for the E22's `TXEN` pin: + + 1. It can be connected to the MCU on the pin defined as `SX126X_TXEN` in `variant.h`. In this configuration, the MCU will control Tx/Rx switching "manually". As long as `SX126X_TXEN` and `SX126X_RXEN` are both defined in `variant.h` (and neither is set to `RADIOLIB_NC`), `SX126xInterface.cpp` will initialize the E22 correctly for this mode. + + 2. Alternately, it can be connected to the E22's `DIO2` pin only, with neither `TXEN` nor `DIO2` being connected to the MCU. In this configuration, the E22 will control Tx/Rx switching automatically. In `variant.h`, as long as `SX126X_TXEN` is defined as `RADIOLIB_NC`, and `SX126X_RXEN` is defined and connected to the E22's `RXEN` pin, and `E22_TXEN_CONNECTED_TO_DIO2` is defined, `SX126xInterface.cpp` will initialize the E22 correctly for this mode. This configuration frees up a GPIO, and presents no drawbacks that I have found. + + - Note that any combination other than the two described above will likely result in unexpected behavior. In my testing, some of these other configurations appeared to "work" at first glance, but every one I tried had at least one of the following flaws: weak Tx power, extremely poor Rx sensitivity, or the E22 overheating because TXEN was never pulled low, causing its PA to stay on indefinitely. + + - Along the same lines, it is a good idea to check the E22's temperature frequently by lightly touching the shield. If you feel the shield getting hot (i.e. approaching uncomfortable to touch) near pins 1, 2, and 3, something is probably misconfigured; disconnect both the Xiao BLE and E22 from power and double check wiring and pin mapping. + + - Whether you opt to let the E22 control Rx and Tx or handle this manually, the E22's `RXEN` pin must always be connected to the MCU on the pin defined as `SX126X_RXEN` in `variant.h`. + +

Note

+ +The default pin mapping in `variant.h` uses 'automatic Tx/Rx switching' mode. If you wire your board for manual Rx/Tx switching, make sure to update `variant.h` accordingly by commenting/uncommenting the necessary lines in the 'E22 Tx/Rx control options' section. + +  + +--- + +  + +

Example wiring for "E22 automatic Tx/Rx switching" mode:

+  + +MCU -> E22 connections +| Xiao BLE pin | variant.h definition | E22 pin | Notes | +| :------------ | :---------------------------- | :-----------------| :------------------------------------------------------------------------------------------------------------------- | +| D0 | SX126X_CS | 19 (NSS) | | +| D1 | SX126X_DIO1 | 13 (DIO1) | | +| D2 | SX126X_BUSY | 14 (BUSY) | | +| D3 | SX126X_RESET | 15 (NRST) | | +| D7 | SX126X_RXEN | 6 (RXEN) | These pins must still be connected, and `SX126X_RXEN` defined in `variant.h`, otherwise Rx sensitivity will be poor. | +| D8 | PIN_SPI_SCK | 18 (SCK) | | +| D9 | PIN_SPI_MISO | 16 (MISO) | | +| D10 | PIN_SPI_MOSI | 17 (MOSI) | | + +  +  + +E22 -> E22 connections: +| E22 pin | E22 pin | Notes | +| :------------ | :---------------------------- | :------------------------------------------------------------------------ | +| TXEN | DIO2 | These must be physically connected for automatic Tx/Rx switching to work. | + +

Note

+ +The schematic (`xiao-ble-e22-schematic.png`) in the `eagle-project` directory uses this wiring. + +  + +--- + +  + +

Example wiring for "Manual Tx/Rx switching" mode:

+ +MCU -> E22 connections +| Xiao BLE pin | variant.h definition | E22 pin | Notes | +| :------------ | :---------------------------- | :-----------------| :--------------------------- | +| D0 | SX126X_CS | 19 (NSS) | | +| D1 | SX126X_DIO1 | 13 (DIO1) | | +| D2 | SX126X_BUSY | 14 (BUSY) | | +| D3 | SX126X_RESET | 15 (NRST) | | +| D6 | SX126X_TXEN | 7 (TXEN) | | +| D7 | SX126X_RXEN | 6 (RXEN) | | +| D8 | PIN_SPI_SCK | 18 (SCK) | | +| D9 | PIN_SPI_MISO | 16 (MISO) | | +| D10 | PIN_SPI_MOSI | 17 (MOSI) | | + +E22 -> E22 connections: (none) + +  + +## 5. Flash the firmware to the Xiao BLE + +1. Double press the Xiao's `reset` button to put it in bootloader mode. +2. In a terminal window, navigate to the Meshtastic firmware repo's root directory, and from there to `.pio/build/xiao_ble`. +3. Convert the generated `.hex` file into a `.uf2` file: + + ```bash + ../../../bin/uf2conv.py firmware.hex -c -o firmware.uf2 -f 0xADA52840 + ``` + +4. Copy the new `.uf2` file to the Xiao's mass storage volume: + + ```bash + cp firmware.uf2 /Volumes/XIAO-BOOT + ``` + +5. The Xiao's red LED will flash for several seconds as the firmware is copied. +6. Once the firmware is copied, to verify it is running, run the following command: + + ```bash + meshtastic --noproto + ``` + +7. Then, press the Xiao's `reset` button again. You should see a lot of debug output logged in the terminal window. + +  + +## 6. Troubleshooting + +- If after flashing Meshtastic, the Xiao is bootlooped, look at the serial output (you can see this by running `meshtastic --noproto` with the device connected to your computer via USB). + + - If you see that the SX1262 init result was -2, this likely indicates a wiring problem; double check your wiring and pin mapping in `variant.h`. + + - If you see an error mentioning tinyFS, this may mean you need to reformat the Xiao's storage: + + 1. Double press the `reset` button to put the Xiao in bootloader mode. + + 2. In a terminal window, navigate to the Meshtastic firmware repo's root directory, and from there to `variants/xiao_ble`. + + 3. Run the following command:  `cp xiao-ble-internal-format.uf2 /Volumes/XIAO-BOOT` + + 4. The Xiao's red LED will flash briefly as the filesystem format firmware is copied. + + 5. Run the following command:  `meshtastic --noproto` + + 6. In the output of the above command, you should see a message saying "Formatting...done". + + 7. To flash Meshtastic again, repeat the steps in section 5 above. + + - If you don't see any specific error message, but the boot process is stuck or not proceeding as expected, this might also mean there is a conflict in `variant.h`. If you have made any changes to the pin mapping, ensure they do not result in a conflict. If all else fails, try reverting your changes and using the known-good configuration included here. + + - The above might also mean something is wired incorrectly. Try reverting to one of the known-good example wirings in section 4. + +- If the E22 gets hot to the touch: + - The power amplifier is likely running continually. Disconnect it and the Xiao from power immediately, and double check wiring and pin mapping. In my experimentation this occurred in cases where TXEN was inadvertenly high (usually due to a pin mapping conflict). + +  + +## 7. Notes + +- There are several anecdotal recommendations regarding the Tx power the E22's internal SX1262 should be set to in order to achieve the advertised output of 30 dBm, ranging from 4 (per this article in the RadioLib github repo) to 22 (per this conversation from the Meshtastic Discord). When paired with the Xiao BLE in the configurations described above, I observed that the output is at its maximum when Tx power is set to 22. + +- To achieve its full output, the E22 should have a bypass capacitor from its 5V supply to ground. 100 µF works well. + +- The E22 will happily run on voltages lower than 5V, but the full output power will not be realized. For example, with a fully charged LiPo at 4.2V, Tx power appears to max out around 26-27 dBm. + +  + +## 8. Testing Methodology + +During what became a fairly long trial-and-error process, I did a lot of careful testing of Tx power and Rx sensitivity. My methodology in these tests was as follows: + +- All tests were conducted between two nodes: + + 1. The Xiao BLE + E22 coupled with an Abracon ARRKP4065-S915A ceramic patch antenna + + 2. A RAK 5005/4631 coupled with a Laird MA9-5N antenna via a 4" U.FL to Type N pigtail. + + - No other nodes were powered up onsite or nearby. + +
+ +- Each node and its antenna was kept in exactly the same position and orientation throughout testing. + +- Other environmental factors (e.g. the location and resting position of my body in the room while testing) were controlled as carefully as possible. + +- Each test comprised at least five (and often ten) runs, after which the results were averaged. + +- All testing was done by sending single-character messages between nodes and observing the received RSSI reported in the message acknowledgement. Messages were sent one by one, waiting for each to be acknowledged or time out before sending the next. + +- The E22's Tx power was observed by sending messages from the RAK to the Xiao BLE + E22 and recording the received RSSI. + +- The opposite was done to observe the E22's Rx sensitivity: messages were sent from the Xiao BLE + E22 to the RAK, and the received RSSI was recorded. + +While this cannot match the level of accuracy achievable with actual test equipment in a lab setting, it was nonetheless sufficient to demonstrate the (sometimes very large) differences in Tx power and Rx sensitivity between various configurations. diff --git a/variants/xiao_ble/platformio.ini b/variants/xiao_ble/platformio.ini new file mode 100644 index 0000000..6c47780 --- /dev/null +++ b/variants/xiao_ble/platformio.ini @@ -0,0 +1,14 @@ +; Seeed Xiao BLE: https://www.digikey.com/en/products/detail/seeed-technology-co-ltd/102010448/16652893 +[env:xiao_ble] +extends = nrf52840_base +board = xiao_ble_sense +board_level = extra +build_flags = ${nrf52840_base.build_flags} -Ivariants/xiao_ble -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 -D EBYTE_E22 -DEBYTE_E22_900M30S -DPRIVATE_HW + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" +board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/xiao_ble> +lib_deps = + ${nrf52840_base.lib_deps} +debug_tool = jlink +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +;upload_protocol = jlink diff --git a/variants/xiao_ble/variant.cpp b/variants/xiao_ble/variant.cpp new file mode 100644 index 0000000..2c6c3e5 --- /dev/null +++ b/variants/xiao_ble/variant.cpp @@ -0,0 +1,55 @@ +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // D0 .. D13 + 2, // D0 is P0.02 (A0) + 3, // D1 is P0.03 (A1) + 28, // D2 is P0.28 (A2) + 29, // D3 is P0.29 (A3) + 4, // D4 is P0.04 (A4,SDA) + 5, // D5 is P0.05 (A5,SCL) + 43, // D6 is P1.11 (TX) + 44, // D7 is P1.12 (RX) + 45, // D8 is P1.13 (SCK) + 46, // D9 is P1.14 (MISO) + 47, // D10 is P1.15 (MOSI) + + // LEDs + 26, // D11 is P0.26 (LED RED) + 6, // D12 is P0.06 (LED BLUE) + 30, // D13 is P0.30 (LED GREEN) + 14, // D14 is P0.14 (READ_BAT) + + // LSM6DS3TR + 40, // D15 is P1.08 (6D_PWR) + 27, // D16 is P0.27 (6D_I2C_SCL) + 7, // D17 is P0.07 (6D_I2C_SDA) + 11, // D18 is P0.11 (6D_INT1) + + // MIC + 42, // 17,//42, // D19 is P1.10 (MIC_PWR) + 32, // 26,//32, // D20 is P1.00 (PDM_CLK) + 16, // 25,//16, // D21 is P0.16 (PDM_DATA) + + // BQ25100 + 13, // D22 is P0.13 (HICHG) + 17, // D23 is P0.17 (~CHG) + + // + 21, // D24 is P0.21 (QSPI_SCK) + 25, // D25 is P0.25 (QSPI_CSN) + 20, // D26 is P0.20 (QSPI_SIO_0 DI) + 24, // D27 is P0.24 (QSPI_SIO_1 DO) + 22, // D28 is P0.22 (QSPI_SIO_2 WP) + 23, // D29 is P0.23 (QSPI_SIO_3 HOLD) + + // NFC + 9, // D30 is P0.09 (NFC1) + 10, // D31 is P0.10 (NFC2) + + // VBAT + 31, // D32 is P0.10 (VBAT) +}; \ No newline at end of file diff --git a/variants/xiao_ble/variant.h b/variants/xiao_ble/variant.h new file mode 100644 index 0000000..a86ddfd --- /dev/null +++ b/variants/xiao_ble/variant.h @@ -0,0 +1,212 @@ +#ifndef _SEEED_XIAO_NRF52840_SENSE_H_ +#define _SEEED_XIAO_NRF52840_SENSE_H_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// #define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +#define PINS_COUNT (33) +#define NUM_DIGITAL_PINS (33) +#define NUM_ANALOG_INPUTS (8) // A6 is used for battery, A7 is analog reference +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs + +#define LED_RED 11 +#define LED_BLUE 12 +#define LED_GREEN 13 + +#define PIN_LED1 LED_GREEN +#define PIN_LED2 LED_BLUE +#define PIN_LED3 LED_RED + +#define PIN_LED PIN_LED1 +#define LED_PWR (PINS_COUNT) + +#define LED_BUILTIN PIN_LED + +#define LED_STATE_ON 1 // State when LED is lit + +/* + * Buttons + */ +#define PIN_BUTTON1 (PINS_COUNT) + +// Digital PINs +#define D0 (0ul) +#define D1 (1ul) +#define D2 (2ul) +#define D3 (3ul) +#define D4 (4ul) +#define D5 (5ul) +#define D6 (6ul) +#define D7 (7ul) +#define D8 (8ul) +#define D9 (9ul) +#define D10 (10ul) + +/* + * Analog pins + */ +#define PIN_A0 (0) +#define PIN_A1 (1) +#define PIN_A2 (2) +#define PIN_A3 (3) +#define PIN_A4 (4) +#define PIN_A5 (5) +#define PIN_VBAT (32) +#define VBAT_ENABLE (14) + +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; +static const uint8_t A3 = PIN_A3; +static const uint8_t A4 = PIN_A4; +static const uint8_t A5 = PIN_A5; +#define ADC_RESOLUTION 12 + +// Other pins +#define PIN_NFC1 (30) +#define PIN_NFC2 (31) + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (-1) // (7) +#define PIN_SERIAL1_TX (-1) // (6) + +#define PIN_SERIAL2_RX (-1) +#define PIN_SERIAL2_TX (-1) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 1 + +#define PIN_SPI_MISO (9) +#define PIN_SPI_MOSI (10) +#define PIN_SPI_SCK (8) + +static const uint8_t SS = D0; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +// supported modules list +#define USE_SX1262 + +// common pinouts for SX126X modules +#define SX126X_CS D0 +#define SX126X_DIO1 D1 +#define SX126X_BUSY D2 +#define SX126X_RESET D3 + +// ---------------------------------------------------------------- + +// E22 Tx/Rx control options: + +// 1. Let the E22 control Tx and Rx automagically via DIO2. + +// * The E22's TXEN and DIO2 pins are connected to each other, but not to the MCU. +// * The E22's RXEN pin *is* connected to the MCU. +// * E22_TXEN_CONNECTED_TO_DIO2 is defined so the logic in SX126XInterface.cpp handles this configuration correctly. + +#define SX126X_TXEN RADIOLIB_NC +#define SX126X_RXEN D7 + +// ------------------------------ OR ------------------------------ + +// 2. Control Tx and Rx manually. + +// * The E22's TXEN and RXEN pins are both connected to the MCU. + +// #define SX126X_TXEN D6 +// #define SX126X_RXEN D7 + +// ---------------------------------------------------------------- + +#ifdef EBYTE_E22 +// Internally the TTGO module hooks the SX126x-DIO2 in to control the TX/RX switch +// (which is the default for the sx1262interface code) +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#ifdef EBYTE_E22_900M30S +// 10dB PA gain and 30dB rated output; based on PA output table from Ebyte Robin +#define REGULATORY_GAIN_LORA 10 +#define SX126X_MAX_POWER 20 +#endif +#ifdef EBYTE_E22_900M33S +// 25dB PA gain and 33dB rated output; based on TX Power Curve from E22-900M33S_UserManual_EN_v1.0.pdf +#define REGULATORY_GAIN_LORA 25 +#define SX126X_MAX_POWER 8 +#endif +#endif + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 // 2 + +#define PIN_WIRE_SDA (4) +#define PIN_WIRE_SCL (5) + +static const uint8_t SDA = PIN_WIRE_SDA; +static const uint8_t SCL = PIN_WIRE_SCL; + +#define PIN_LSM6DS3TR_C_POWER (15) +#define PIN_LSM6DS3TR_C_INT1 (18) + +// PDM Interfaces +// --------------- +#define PIN_PDM_PWR (19) +#define PIN_PDM_CLK (20) +#define PIN_PDM_DIN (21) + +// QSPI Pins +#define PIN_QSPI_SCK (24) +#define PIN_QSPI_CS (25) +#define PIN_QSPI_IO0 (26) +#define PIN_QSPI_IO1 (27) +#define PIN_QSPI_IO2 (28) +#define PIN_QSPI_IO3 (29) + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES P25Q16H +#define EXTERNAL_FLASH_USE_QSPI + +// Battery + +#define BAT_READ \ + 14 // 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 10 +#define CHARGE_LED 23 // P0_17 = 17 D23 YELLOW CHARGE LED +#define HICHG 22 // P0_13 = 13 D22 Charge-select pin for Lipo for 100 mA instead of default 50mA charge + +// The battery sense is hooked to pin A0 (5) +#define BATTERY_PIN PIN_VBAT // PIN_A0 + +// ratio of voltage divider = 3.0 (R17=1M, R18=510k) +#define ADC_MULTIPLIER 3 // 3.0 + a bit for being optimistic + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif diff --git a/variants/xiao_ble/xiao-ble-internal-format.uf2 b/variants/xiao_ble/xiao-ble-internal-format.uf2 new file mode 100644 index 0000000000000000000000000000000000000000..59de2c68a6a49b308f77a83bb278e89e720860c8 GIT binary patch literal 122880 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 SzGBepAo%~^{dfsb{Qnd2oeWg~ literal 0 HcmV?d00001 diff --git a/variants/xiao_ble/xiao_ble.sh b/variants/xiao_ble/xiao_ble.sh new file mode 100644 index 0000000..2f3cc53 --- /dev/null +++ b/variants/xiao_ble/xiao_ble.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# adapted from the script linked in this very helpful article: https://enzolombardi.net/low-power-bluetooth-advertising-with-xiao-ble-and-platformio-e8e7d0da80d2 + +# source: https://gist.githubusercontent.com/turing-complete-labs/b3105ee653782183c54b4fdbe18f411f/raw/d86779ba7702775d3b79781da63d85442acd9de6/xiao_ble.sh +# download the core for arduino from seeedstudio. Softdevice 7.3.0, linker and variants folder are what we need +curl https://files.seeedstudio.com/arduino/core/nRF52840/Arduino_core_nRF52840.tar.bz2 -o arduino.core.1.0.0.tar.bz2 +tar -xjf arduino.core.1.0.0.tar.bz2 +rm arduino.core.1.0.0.tar.bz2 + +# copy the needed files +cp 1.0.0/cores/nRF5/linker/nrf52840_s140_v7.ld ~/.platformio/packages/framework-arduinoadafruitnrf52/cores/nRF5/linker +cp -r 1.0.0/cores/nRF5/nordic/softdevice/s140_nrf52_7.3.0_API ~/.platformio/packages/framework-arduinoadafruitnrf52/cores/nRF5/nordic/softdevice + +rm -rf 1.0.0 +echo done! diff --git a/variants/xiao_ble/xiao_nrf52840_ble_bootloader-0.7.0-22-g277a0c8_s140_7.3.0.zip b/variants/xiao_ble/xiao_nrf52840_ble_bootloader-0.7.0-22-g277a0c8_s140_7.3.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..40b966bafe77aaca21d5408973ae8128e5040e01 GIT binary patch literal 192586 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Μ) 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 d;a^LvLT2F-S5zRGTtOKN_&

jB+gsIShfb^J|j-@I6}Biu!8`{#DuHQKH`wOy0Y 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